DevToolBox

見えない文字(ZWSP・BOM・NBSP)が起こすバグの見つけ方

最終更新日: 2026-08-21公開日: 2026-06-11執筆: DevToolBox編集部

Web からコピペしたコードが SyntaxError: Invalid or unexpected token で落ちる。 画面上は完全に同じ文字列なのに === が false。grep しても絶対あるはずの行がヒットしない—— こうした「目で見ても分からないバグ」の犯人は、ほぼ間違いなく不可視の Unicode 文字です。 本記事では U+200B(ゼロ幅スペース)・U+FEFF(BOM)・U+00A0(ノーブレークスペース)などの混入経路と、 エディタ・正規表現・od/hexdump を使った特定・除去の手順を解説します。

Invisible Unicode characters such as the zero-width space (U+200B), the BOM (U+FEFF) and the no-break space (U+00A0) sneak into code via copy-paste and break parsers, string comparison, grep searches and CSV headers. This guide shows how to detect and remove them.

TL;DR

Unicode Inspector

疑わしい文字列を貼り付けると、1文字ずつコードポイント(U+200B 等)と名前を表示。不可視文字が混ざっていれば一目で特定できます。ブラウザ完結・外部送信なし。

今すぐ試す →

1. 犯人はこの4種類 / The four usual suspects

文字 / CharacterUTF-8 バイト列主な混入経路 / How it gets in
U+200B ZERO WIDTH SPACE (ZWSP)E2 80 8BWeb ページの折り返し制御。ブログ・チャット・スプレッドシートからのコピペ
U+FEFF BOM / ZERO WIDTH NO-BREAK SPACEEF BB BFExcel・メモ帳が保存する UTF-8 ファイルの先頭。CSV / JSON の冒頭に付く
U+00A0 NO-BREAK SPACE (NBSP)C2 A0HTML の  、Mac の Option+Space、Word からのコピペ
U+200C / U+200D ゼロ幅(非)結合子 (ZWNJ / ZWJ)E2 80 8C / E2 80 8D絵文字の結合シーケンス、アラビア文字等の組版。SNS からのコピペ

いずれも幅ゼロまたは普通の空白と同じ見た目で描画されるため、目視では発見できません。 なお U+FEFF はファイル先頭にあれば BOM、途中にあればゼロ幅ノーブレークスペースとして扱われます (文中での使用は非推奨で、現在は U+2060 WORD JOINER が代替)。

All four render as nothing or as an ordinary space, so you cannot spot them visually. U+FEFF acts as a BOM at the start of a file and as a zero-width no-break space elsewhere.

1-2. その他の要注意文字7種 / Seven more troublemakers

上の4種以外にも、実務でよく事故を起こす不可視・特殊空白文字があります。混入経路と実際に壊れるものを一覧にしました。

文字どこから混入するか何が壊れるか
U+00A0 NBSPWord/GoogleドキュメントからのコピペUnicodeテキストのコピー、HTMLの  、Macの Option+Spaceシェルでコマンドと引数が1語に連結され command not found。CSSの空白判定ずれ
U+200B ZWSPブログ・ニュースサイトの折り返し制御、チャットアプリの絵文字前後、スプレッドシートのセルコピーJSONやコードの SyntaxError、===比較の不一致、grepが該当行を検出しない
U+200E LRM / U+200F RLMアラビア語・ヘブライ語混じりのWebページや翻訳ツールからのコピペ左右混在テキストのレイアウト崩れ、文字列の途中に混入してのSyntaxError
U+FEFF BOM / ZWNBSPExcel保存のUTF-8 CSV、メモ帳、一部エディタの既定保存設定JSON.parseのUnexpected token、CSVヘッダの1列目だけundefined
U+3000 全角空白日本語入力の変換ミス、Excelのセル内インデント、Wordの字下げtrim()後も残り文字列比較が不一致。半角スペース前提の正規表現がマッチしない
U+2028 LINE SEPARATOR / U+2029 PARAGRAPH SEPARATORmacOSのリッチテキストコピペ、一部CMSの本文出力、PDFのテキスト抽出JSON文字列リテラル内に混じるとJavaScriptのSyntaxError(JSONとしては合法でもJSとしては非合法)。古いJSパーサやeval経由コードが壊れる

Beyond the four usual suspects: NBSP breaks shell command parsing, LRM/RLM garble bidi text and can sit mid-identifier, U+3000 (ideographic space) survives trim() and defeats ASCII-only regexes, and U+2028/U+2029 are valid inside a JSON string but illegal as raw line terminators in JavaScript source — so a JSON payload containing them can throw when embedded directly in JS via eval or an older parser.

2. 典型的なバグ症状4つ / Four classic symptoms

2-1. コピペ由来のシンタックスエラー

見た目はまったく正しいコードなのにパーサーが落ちます。実際のエラーメッセージは次のとおりです。

// JavaScript (Node.js / Chrome) — 行内に U+200B が混入
SyntaxError: Invalid or unexpected token

# Python 3 — こちらはコードポイントを教えてくれる
SyntaxError: invalid non-printable character U+200B

# シェル — コマンドと引数の間が NBSP だと1語に連結される
bash: curl -X: command not found

Python 3 は問題のコードポイントを明示してくれますが、JavaScript の V8 は 「どの文字か」を教えてくれません。エラー行を一度削除して手で打ち直すのが最速の応急処置です。

2-2. 文字列比較の不一致

const a = "admin";        // 5文字
const b = "admin\u200B";   // 末尾に ZWSP。画面表示は a と同一
console.log(a === b);      // false
console.log(a.length, b.length); // 5 6  ← 長さの差で気付ける

2-3. grep / エディタ検索でヒットしない

検索語を手入力し、対象ファイル側に adm​in のように不可視文字が挟まっていると、 「確実に存在する行」が検索にかからないという不気味な現象になります。逆方向(検索語側に混入)もあります。

2-4. CSV ヘッダ不一致(BOM)

// Excel が出力した UTF-8 CSV を読むと…
Object.keys(rows[0]);  // ["\uFEFFid", "name", "price"]
rows[0]["id"];          // undefined ← 1列目だけ取れない

# Python (pandas) は utf-8-sig で BOM を自動除去
df = pd.read_csv("data.csv", encoding="utf-8-sig")

Symptoms: parsers reject visually correct code, identical-looking strings compare unequal, grep misses lines that clearly exist, and the first CSV column comes back undefined because the header is actually "id".

3. 検出方法 / How to detect them

3-1. エディタで可視化する

VS Code 1.63 以降は editor.unicodeHighlight.invisibleCharacters が既定で有効になっており、 不可視文字が黄色の枠でハイライトされます(表示されない場合は settings.json で true を確認)。 あわせて "editor.renderWhitespace": "all" にすると NBSP と通常スペースの描画差も見えます。

3-2. 正規表現で機械的に探す

// JavaScript: 主要な不可視文字をまとめて検出
const INVISIBLE = /[\u00A0\u200B-\u200D\u2060\uFEFF]/g;
console.log(INVISIBLE.test(suspiciousText)); // true なら混入あり

// 1文字ずつコードポイントを16進ダンプして目視確認
[..."adm\u200Bin"].map((c) => c.codePointAt(0).toString(16));
// → ["61", "64", "6d", "200b", "69", "6e"]

3-3. コマンドラインで探す

# GNU grep (-P: PCRE) でリポジトリ全体を走査
grep -rnP "[\x{200B}\x{FEFF}\x{00A0}\x{200C}\x{200D}]" src/

# od で生バイトを確認(E2 80 8B = U+200B, C2 A0 = U+00A0)
printf '%s' "adm​in" | od -An -tx1
#  61 64 6d e2 80 8b 69 6e

# ファイル先頭3バイトが EF BB BF なら BOM 付き UTF-8
head -c 3 data.csv | od -An -tx1

Detect them with VS Code's built-in Unicode highlighting, a regex like/[ ​-‍⁠]/g, grep -P with hex escapes, or by dumping raw bytes with od -An -tx1 and looking for E2 80 8B / EF BB BF / C2 A0.

3-4. 環境別コマンド実例 / Detection commands by environment

VS Code の設定・シェルでのバイト確認・Python での確認、それぞれの実行例です。

# VS Code settings.json — 不可視文字を黄色枠でハイライト
{
  "editor.unicodeHighlight.invisibleCharacters": true,
  "editor.unicodeHighlight.ambiguousCharacters": true,
  "editor.renderWhitespace": "all"
}
# コマンドパレットからも: "Unicode Highlight" で検索
# macOS/Linux: od でバイト列を確認
$ printf '%s' "adm​in" | od -An -tx1
 61 64 6d e2 80 8b 69 6e
#              ^^^^^^^^ E2 80 8B = U+200B が挟まっている

# hexdump -C の場合(アドレスとASCII表示付き)
$ printf '%s' "adm​in" | hexdump -C
00000000  61 64 6d e2 80 8b 69 6e                          |adm...in|
# Python: repr() で不可視文字を \u 表記として可視化
>>> s = "admin"  # クリップボードから貼り付けた文字列
>>> repr(s)
"'adm\u200bin'"
>>> [hex(ord(c)) for c in s]
['0x61', '0x64', '0x6d', '0x200b', '0x69', '0x6e']

VS Code: enable editor.unicodeHighlight.invisibleCharacters and renderWhitespace: all. Shell: pipe text through od -An -tx1 or hexdump -C and look for E2 80 8B (ZWSP) or C2 A0 (NBSP). Python: repr() prints hidden characters as explicit \\u escapes, and [hex(ord(c)) for c in s] gives a per-character codepoint dump.

3-5. 実際に起きた事故シナリオ / Real-world incident scenarios

シナリオA: コピペしたcurlコマンドが command not found になる

  1. 技術ブログやSlackのコードブロックから curl -X POST https://example.com/api をコピー
  2. ターミナルに貼り付けて実行すると bash: curl -X: command not found というエラー
  3. 原因はオプションとURLの間の空白が実は U+00A0(NBSP)だったこと。シェルはNBSPを引数区切りとして認識せず、-X 以降を1つのトークンとして扱う
  4. 確認: echo -n '貼り付けた行' | od -An -tx1 で該当箇所が c2 a0 になっていないか見る
  5. 対処: 一度エディタに貼ってから sed や置換で NBSP を通常スペースに変換してから再実行する、または手打ちし直す

シナリオB: JSON文字列に混じった U+2028 が古いJSパーサを壊す

  1. ユーザーが投稿フォームに、macOSのリッチテキストエディタから本文をコピペ(改行に U+2028 が使われることがある)
  2. サーバーはその文字列をそのままJSONとしてAPIレスポンスに含める。JSONとしては U+2028 は文字列内に置ける合法な文字なので JSON.parse 自体は成功する
  3. しかしフロント側でそのJSONを eval() や、テンプレートに直接埋め込んで実行する古いコード(例: "const data = " + jsonText のような文字列結合)を通すと、U+2028 が行終端文字として解釈され SyntaxError: Unexpected token ILLEGAL になる
  4. 確認: 問題の文字列に対して text.includes(String.fromCharCode(0x2028)) を実行して true が返るか確認する(U+2029 も同様に 0x2029 で調べる)
  5. 対処: JSON文字列化の際に U+2028 / U+2029 を Unicodeエスケープ表記(バックスラッシュ + u2028)へ置換してから埋め込むか、eval経由の埋め込みをやめて JSON.parse に統一する。多くのテンプレートエンジンやフレームワークはこの置換を自動で行う

シナリオC: コピー&ペーストで書いたコードにNBSPが混入し続ける

Web上のドキュメントやAIチャットの回答からコードをコピーして貼り付けると、 インデント用の空白や単語間のスペースがNBSPに置き換わっていることがあります。 見た目もエディタの構文ハイライトも正常なため、保存時にはエラーが出ず、 後になってビルドや文字列置換処理が「該当箇所が見つからない」と失敗してから気づく、というケースが典型です。 コードを扱うときはコピペ由来のテキストを疑い、貼り付け直後に一度Unicodeハイライト表示を確認する習慣が有効です。

Scenario A: a copied curl command fails with command not found because the space after -X is actually U+00A0, which the shell does not treat as an argument separator. Scenario B: text copied from a rich-text editor can carry U+2028 line separators; they are legal inside a JSON string, so JSON.parse succeeds, but embedding that JSON via eval() or string concatenation throws a SyntaxError because U+2028 is a line terminator in JavaScript source. Scenario C: pasting code snippets from web pages or AI chat answers can silently swap ordinary spaces for NBSP — the syntax highlighting looks fine, so the bug only surfaces later when a build or string match fails.

4. 除去と再発防止 / Removal & prevention

// ゼロ幅系をすべて削除し、NBSP は通常スペースに置換
const clean = s
  .replace(/[\u200B-\u200D\u2060\uFEFF]/g, "")
  .replace(/\u00A0/g, " ");

// JSON.parse 前は先頭 BOM だけ剥がすのが安全
JSON.parse(text.replace(/^\uFEFF/, ""));

Strip zero-width characters explicitly — trim() removes NBSP and U+FEFF but not U+200B, and NFKC normalization only fixes NBSP. Never strip ZWJ from arbitrary user input, because emoji sequences depend on it; restrict cleanup to identifiers, keys and source code.

まとめ / Summary

「見た目は正しいのに動かない」と感じたら、推測でコードをいじる前にlength の比較とコードポイントのダンプを行うのが最短ルートです。 混入源(Excel・Word・Web ページ・SNS)を特定したら、取り込み口で正規表現による除去を仕込めば再発も防げます。

Invisible Unicode characters are a recurring source of "impossible" bugs. A zero-width space pasted from a blog post makes V8 throw SyntaxError: Invalid or unexpected tokenon a line that looks perfectly fine; Python at least names the culprit withinvalid non-printable character U+200B. The same characters make equal-looking strings fail ===, hide lines from grep, and break CSV parsing when Excel prepends a BOM so the first header becomes "id". Detection is mechanical once you know the byte signatures: E2 80 8B (U+200B), EF BB BF (U+FEFF) and C2 A0 (U+00A0). Use VS Code's Unicode highlighting, scan with grep -rnP "[\x{200B}\x{FEFF}\x{00A0}]", or pipe the text through od -An -tx1. For cleanup, remember that trim()removes NBSP and U+FEFF but not the zero-width space, and that NFKC normalization only converts NBSP — an explicit replace(/[​-‍⁠]/g, "") is required. Do not blanket-strip ZWJ from user input, since emoji sequences rely on it. Finally, save files as UTF-8 without BOM and read Excel CSVs with utf-8-sig to stop the bug at the source.

Unicode Inspector

このページで紹介した検出を GUI で。文字列を貼るだけで U+200B / U+FEFF / U+00A0 などをコードポイント名付きで列挙し、混入位置を特定できます。

今すぐ試す →

よくある質問 / FAQ

コピペしただけのコードで SyntaxError が出るのはなぜ?
Web ページや PDF からのコピーで U+200B(ゼロ幅スペース)や U+00A0(ノーブレークスペース)が混入するためです。見た目は普通のコードでも、パーサーには未知の文字として扱われます。該当行を一度削除して手で打ち直すか、不可視文字を正規表現で除去してください。
同じに見える文字列の比較が false になる原因は?
片方の文字列にゼロ幅スペースなどの不可視文字が含まれていると、表示は同一でも length とコードポイント列が異なるため一致しません。length を比較するか、各文字を codePointAt で16進ダンプすると確認できます。
CSV の1列目だけ row["id"] が undefined になるのはなぜ?
Excel が出力する UTF-8 CSV は先頭に BOM(U+FEFF)が付くため、最初のヘッダ名が "\uFEFFid" になっているのが典型原因です。読み込み時に先頭の U+FEFF を除去するか、Python なら encoding='utf-8-sig' を指定します。
trim() でゼロ幅スペースは消える?
消えません。JavaScript の trim() は U+00A0 や U+FEFF は除去しますが、U+200B(ゼロ幅スペース)は空白扱いではないため残ります。明示的に replace(/[\u200B-\u200D\u2060]/g, '') する必要があります。
Why does copy-pasted code throw a syntax error?
Text copied from web pages or PDFs often carries zero-width spaces (U+200B) or no-break spaces (U+00A0). They are invisible but illegal in source code, so the parser fails. Retype the line or strip the characters with a regex.
How do I find invisible Unicode characters in a file?
Use grep -P "[\x{200B}\x{FEFF}\x{00A0}]", pipe the text through od -An -tx1 to inspect raw bytes, or enable VS Code's built-in Unicode highlighting (editor.unicodeHighlight.invisibleCharacters).

関連ツール / Related tools

関連ガイド / Related guides