Unicode 正規化と見えない文字
同じに見えるテキストが、いつも同じとは限りません。Unicode はひとつの見える文字を複数の方法で組み立てられるようにし、何も描画しない文字を許し、画面上では区別できないのに中身は異なる組み合わせを含んでいます。本ガイドではその違いの出どころと、ログイン失敗・重複レコード・なりすましドメインに化ける前に何をすべきかを説明します。
コードポイント、バイト、そして「長さ」の意味
ひとつの文字列には少なくとも3通りのもっともらしい長さがあり、値は一致しません。"héllo" は読み手には5文字ですが、é の組み立て方によってコードポイントでは5か6、UTF-8 バイトでは6か7です。絵文字はその差をさらに広げます。家族の絵文字は見えない結合子でつないだ複数の人物から組み立てられた1枚の絵で、多くの言語はその長さを2〜11のどこかとして報告します。
問いに合った単位を選びましょう。データベースの列制限や HTTP ヘッダーが測るのはバイト長です。多くの文字列 API が返すのはコードポイント数。人が「文字数」と言うときに指しているのは書記素クラスタ数——カーソルが1つ進む単位——であり、切り詰めがその境界に着地するかどうかが、きれいな切断と壊れた絵文字を分けます。
同じテキスト、2つの符号化:NFC と NFD
"é" は単一のコードポイント(U+00E9)にも、素の "e" に結合アキュート(U+0301)が続く2つにもなりえます。描画は同一でも並びは別物なので、素朴な等価判定は「違う文字列」と答え、データベースの一意インデックスは両方を受け入れ、片方で検索するともう片方を取りこぼします。正規化とは正準形を選ぶことです。NFC は単一コードポイントへ合成し、NFD は基底文字と結合記号へ分解します。
これは珍しい話ではありません。ある環境で入力し別の環境から貼り付けたテキストは日常的に食い違います——macOS は歴史的にファイル名を分解形で保存してきた一方、ウェブの多くは合成形を送ります——し、ハングルにも完成形音節と分離したチャモという同じ分岐があります。保存と伝送の既定は NFC が妥当で、肝心なのはひとつを選び、境界で適用することです。比較が失敗した箇所ごとに振りかけることではありません。
NFKC と NFKD:「ほぼ同じ」を同じと数えるとき
K の付いた形は正準の違いに加えて互換の違いも畳み込みます。NFKC では合字 fi は "fi" に、全角 A は "A" に、上付きの ² は "2" に、ノーブレークスペースは通常の空白になります。識別子・ユーザー名・検索語を比較する場面、つまり2つの綴りが同じ枠を主張できてはいけない場面では、まさに望ましい挙動です。
逆に、任意テキストの表示や保存には望ましくない挙動です。変換が非可逆で失われるものがあるからです。数式用の装飾文字は素の ASCII へ潰れ、書き手が選んだ体裁は消えます。K 形は比較キーを導くために使い、ユーザーに返して表示する原文は別に保持してください。
目に見えない文字
何も描画しないコードポイントは数多くあります。ファイル先頭のバイト順マーク(BOM)は JSON 文書の最初のキーや CSV の最初のヘッダーを壊します。ゼロ幅スペースや結合子は文書からのコピー&ペーストを生き延び、完全一致を静かに壊します。ノーブレークスペースは空白に見えて空白分割に掛からず、ソフトハイフンは行が折り返されたときだけ現れます。
散らかっている以上に厄介なものもあります。双方向オーバーライド文字は、コンパイラが読む内容を変えずにソースコードの「表示」順を並べ替えられます——画面上はコメント、中身は実行されるコードという「Trojan Source」の手口です。そして同形異字——キリル文字の а、ギリシャ文字の ο、全角文字——は、ドメインやユーザー名を見慣れたものとそっくりに見せます。文字列が信頼境界を越えるなら、目を信じずコードポイントを調べましょう。
テキストを安全に保つ規則
境界で一度だけ正規化しましょう。入ってくるテキストをシステム入口で NFC に変換してそのまま保存すれば、以後の内部比較は同種同士の比較になります。同一性として使う値——ユーザー名、メールのローカル部、スラッグ、参照キー——は NFKC にケースフォールディングを加えた比較キーを別に作り、表示用の値ではなくそのキーに一意性を課してください。
そのうえで、何を許すかを明示的に決めます。BOM は取り除き、含まれる理由のないフィールドではゼロ幅文字や双方向制御文字を拒否または除去し、文字体系が混在した識別子は気が利いているのではなく疑わしいものとして扱う。どれも高くつく処理ではなく、「正しく入力しているのに特定のユーザーだけログインできない」を調べる時間よりはるかに安上がりです。
- 保存・伝送は NFC、比較キーは NFKC + ケースフォールディング。
- 比較のたびではなく、境界で一度だけ正規化する。
- JSON・CSV・設定ファイルを解析する前に BOM を除去する。
- 見た目は正しいのに挙動がおかしいときはコードポイントを調べる。