👻

不可視文字の世界: ゼロ幅スペース、Bidi制御、隠しテキスト

テキストに潜む不可視文字のカタログ。ツールで正体を暴く。

ゼロ幅文字: ZWSP、ZWJ、ZWNJ

Unicode にはゼロ幅を占める文字が含まれています — テキストデータ内に存在しますが、可視的なグリフは生成しません。最も重要な3つは:

文字コードポイント名前目的
(不可視)U+200Bゼロ幅スペース(ZWSP)任意の改行位置の指示
(不可視)U+200Dゼロ幅接合子(ZWJ)隣接文字を合字/シーケンスに結合
(不可視)U+200Cゼロ幅非接合子(ZWNJ)本来発生する結合を防止

ZWSP(U+200B)はタイ語、クメール語、CJK テキストなど単語間にスペースを使用しないスクリプトで改行可能位置を示すために使用されます。またユーザー名やメッセージでの「見えないスペース」として頻繁に(誤)使用されます。

ZWJ(U+200D)は複合絵文字シーケンスの接着剤です。家族絵文字 👨‍👩‍👧‍👦 は文字通り 男性 + ZWJ + 女性 + ZWJ + 女の子 + ZWJ + 男の子 です。デーヴァナーガリーなどのスクリプトでも子音結合の制御に不可欠です。

ZWNJ(U+200C)は逆の働きをします: 文字の結合を防止します。ペルシャ語やアラビア語では、単語中の文字の非結合形式を示すために使用され、場合によっては意味が変わります。

Bidi オーバーライド: 不可視のテキスト方向制御

Unicode は双方向テキスト(英語のような左から右のスクリプトとアラビア語のような右から左のスクリプトの混在)をサポートしています。これには不可視の制御文字が必要です:

文字コードポイント名前効果
(不可視)U+200ELeft-to-Right Mark(LRM)LTR 方向を強制
(不可視)U+200FRight-to-Left Mark(RLM)RTL 方向を強制
(不可視)U+202ALeft-to-Right Embedding(LRE)LTR 埋め込みを開始
(不可視)U+202BRight-to-Left Embedding(RLE)RTL 埋め込みを開始
(不可視)U+202CPop Directional Formatting(PDF)埋め込みを終了
(不可視)U+202DLeft-to-Right Override(LRO)全テキストを LTR に強制
(不可視)U+202ERight-to-Left Override(RLO)全テキストを RTL に強制
(不可視)U+2066Left-to-Right Isolate(LRI)LTR テキストを分離
(不可視)U+2067Right-to-Left Isolate(RLI)RTL テキストを分離
(不可視)U+2069Pop Directional Isolate(PDI)分離を終了

Right-to-Left Override(U+202E)は特に危険です。後続の全テキストを右から左にレンダリングさせ、ファイル名、コード、URL が実際の内容とまったく異なるものに見える可能性があります:

// 通常のテキスト:
"hello.txt"

// RLO を挿入:
"\u202Ehello.txt"
// 表示: txt.olleh
// "\u202Efdp.exe" というファイル名は "exe.pdf" と表示される!

タグ文字: 隠された完全なアルファベット

Unicode ブロック U+E0000〜U+E007F にはタグ文字が含まれています — 元々言語タグ用に設計された ASCII 文字の不可視版です。その目的では非推奨になりましたが、後に絵文字の旗の地域区分シーケンス(スコットランドの旗 🏴󠁧󠁢󠁳󠁣󠁴󠁿 など)に再利用されました。

タグ文字コードポイント対応する文字
TAG LATIN SMALL LETTER AU+E0061a
TAG LATIN SMALL LETTER BU+E0062b
TAG DIGIT ZEROU+E00300
CANCEL TAGU+E007F(シーケンス終端)

スコットランドの旗絵文字は: 🏴 + TAG g + TAG b + TAG s + TAG c + TAG t + CANCEL TAG。1つの旗絵文字に7コードポイント(14 UTF-16 コードユニット)、うち6つが不可視文字です。

タグ文字は一見無害な文字列の中に任意のテキストを隠すために悪用される可能性があります。不可視でありほとんどのツールが表示しないため、隠しメッセージや電子透かしを運ぶことができます。

// スコットランドの旗を分解:
"🏴󠁧󠁢󠁳󠁣󠁴󠁿"
// = U+1F3F4(黒旗)
// + U+E0067(tag g)
// + U+E0062(tag b)
// + U+E0073(tag s)
// + U+E0063(tag c)
// + U+E0074(tag t)
// + U+E007F(cancel tag)

[..."🏴󠁧󠁢󠁳󠁣󠁴󠁿"].length  // 14(サロゲート含む)

スペース動物園: 18種類の異なるスペース文字

通常のスペース(U+0020)とゼロ幅スペースの他に、Unicode には異なる幅のスペース文字が多数含まれています:

名前コードポイント
SPACEU+0020通常の単語間スペース
NO-BREAK SPACEU+00A0スペースと同じだが改行を防止
EN QUADU+2000en 幅(em の半分)
EM QUADU+2001em 幅
EN SPACEU+2002en 幅
EM SPACEU+2003em 幅
THREE-PER-EM SPACEU+20041/3 em
FOUR-PER-EM SPACEU+20051/4 em
SIX-PER-EM SPACEU+20061/6 em
FIGURE SPACEU+2007数字の幅
PUNCTUATION SPACEU+2008ピリオドの幅
THIN SPACEU+2009約1/5 em
HAIR SPACEU+200A極細スペース
ZERO WIDTH SPACEU+200B幅なし
NARROW NO-BREAK SPACEU+202F狭い、改行なし
MEDIUM MATHEMATICAL SPACEU+205F4/18 em
IDEOGRAPHIC SPACEU+3000CJK 全角スペース
OGHAM SPACE MARKU+1680オガム文字の単語区切り

ノーブレークスペース(U+00A0)は最もよく遭遇する問題スペースです。通常のスペースと見た目は同一ですが改行を防止します。PDF、Word文書、ウェブページからテキストをコピーする際によく現れ、文字列比較をサイレントに失敗させます。

実用上の影響: 不可視文字がバグを引き起こす場所

不可視文字はソフトウェアで実際の問題を引き起こします:

  • 文字列比較の失敗: "hello" === "hello" が、一方に隠れた ZWSP、BOM、またはノーブレークスペースが含まれると false になり得る。
  • JSON/YAML パースエラー: ファイル先頭の BOM(U+FEFF)がパーサーを壊す。キー名内の ZWSP はマッチ不能にする。
  • URL 操作: URL 内の不可視文字がセキュリティフィルターをバイパスしつつユーザーには正当に見える。
  • パスワードフィールド: 不可視文字を含むパスワードをコピー&ペーストすると、ユーザーはパスワードを「知っている」のに一致しない。
  • コードのバグ: 変数名内の ZWNJ や ZWJ が異なる識別子を作成: pricepri‍ce(隠れた ZWJ 付き)は2つの別の変数。
// 一般的な不可視文字の検出:
function hasInvisible(str) {
  const invisible = /[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/;
  return invisible.test(str);
}

// 一般的な不可視文字の除去:
function stripInvisible(str) {
  return str.replace(
    /[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/g,
    ""
  );
}

// 例:
const text = "hello\u200Bworld";
text.length          // 11(10ではない!)
hasInvisible(text)   // true
stripInvisible(text) // "helloworld"

このツールによる可視化

不可視文字の根本的な問題は、設計上不可視であることです。標準的なテキストエディタ、ターミナル、ウェブブラウザはこれらを表示しません。存在を検出するには専用ツールが必要です。

このツールは以下の方法で問題を解決します:

  • すべてのコードポイントを表示: 不可視のものを含め、各コードポイントがグリッド内で独自のセルを持つ。Unicode 名、コードポイント値、一般カテゴリを確認可能。
  • 制御文字のラベル付け: ゼロ幅文字、Bidi 制御、その他の不可視文字は省略名で表示され、即座に識別可能。
  • 書記素クラスタの認識: 不可視文字が可視文字と結合する場合(絵文字内の ZWJ など)、ツールは完全なクラスタ構造を表示。

予期しない挙動のテキストに遭遇したら — 比較が失敗する、長さがおかしい、コピー&ペーストで異なる結果になる — このツールにペーストして実際に何があるか確認してください。

関連記事