SheepDev

エンコード・文字コード

見た目が同じ文字なのに、一致しないのはなぜ?

使うツール

画面に見える文字が同じでも、内部のコードポイント列が同じとは限りません。検索、照合、重複判定で『目では同じなのに一致しない』とき、見た目だけを頼りに何度も入力し直すより、中身を一度表示した方が早いことがあります。Unicode文字情報とUnicode正規化を組み合わせて、小さな違いの正体を探ります。

ただし、似た文字を何でも一つへまとめればよいわけではありません。人名、商品名、数式、識別子では、違いを残す必要があります。正規化は『誤字を直す』機能でも『危険な文字を全部除く』機能でもありません。どの違いを同じとして扱うかを決める材料として使います。

éを二通りに作ってみる

一行ずつ文字情報へ入力
é
é

一行目はU+00E9、二行目はU+0065にU+0301を続けた並びです。通常の表示ではどちらもéに見えますが、コードポイントの数は一対二になります。二行をまとめて入れると改行も表示されるので、まず一つずつ確認します。表示フォントを変えても内部の列は変わらないため、見え方の調整だけでは一致判定を直せません。

二行目の文字列を正規化ツールへ入れると、NFCでは一つのéへ合成され、NFDではeとアクセントの分解形になります。どちらも文字列を比較するための決まった形を作る方式です。NFCを一度適用した結果へもう一度NFCを適用しても変わらない、という確認も試せます。変換前後のコードポイントとUTF-8バイト数を一緒に見ると、何が変わったかを説明できます。

全角Aと丸数字①は、別の判断が必要

互換文字の実験
A①

この入力はNFCではそのままですが、NFKCではA1へ変わります。NFKCは互換上の違いもまとめるため、検索用の表記ゆれ対策に使える場面があります。しかし、丸数字の見た目や全角の幅を残したい文書へそのまま適用すれば、元の表現が失われます。検索用の比較キーと、利用者に見せる原文を別に持つ設計が必要になることもあります。

NFKDも互換上の変換を行いますが、分解形を使います。四つの結果を見て『一番短いものが正しい』と選ばないでください。必要なのは、受け取り側と比較側が同じ方式を使い、その方式で失われる違いを許容できることです。正規化の有無や方式を途中で変える場合は、既存のIDや検索結果への影響も調べます。

ゼロ幅文字は、正規化すれば消えるとは限らない

二行目はaとbの間にU+200Bを含む
ab
a​b

二行目を文字情報で見ると、aとbの間にU+200Bが見つかります。画面では幅がなくても、コードポイントとして存在します。この例はNFCでもNFKCでもabへは変わりません。正規化は、見えない文字をすべて取り除く清掃処理ではないことが分かります。入力制限で許す文字を決める処理とは分けて考えます。

文字情報の分類は、空白、結合文字、制御・書式などを見つけるための手掛かりです。すべての文字の正式名称や安全性を判定する辞書ではありません。ゼロ幅の文字にも言語や表示上の役割があるため、見えないから悪意がある、不要だと一律には決められません。問題の入力だけでなく、正しく扱うべき文字の例も合わせて確認します。

似たaでも、同じアルファベットとは限らない

Latin aとCyrillic а
a
а

一行目はU+0061、二行目はU+0430です。フォントによってよく似て見えますが、別の文字体系の文字であり、NFKCを適用しても同じaへはまとまりません。正規化が済んでいるから、なりすましに使われる似た文字がなくなった、と判断しないでください。URLやアカウント名の安全性には、別の確認が必要です。

反対に、似た文字を独自の対応表ですべて置き換えると、正当な別言語の名前を壊すおそれがあります。検索候補を見つけるための緩い一致と、ログインや権限判定に使う識別子の一致は、同じ基準にするとは限りません。このツールでは文字の違いを確認し、最終的な扱いはサービスの目的に沿って決めます。

変更位置は、編集履歴ではない

正規化結果には、コードポイント列を位置ごとに比べた変更位置が表示されます。一つの文字が分解されて列が長くなると、その後ろの位置もずれるため、複数の変更として見えることがあります。人が何回編集したか、何文字誤入力したかを数える値ではありません。変換後の列全体と、前後の文字を合わせて読んでください。

文字情報のUTF-16位置とコードポイント位置も、常に同じではありません。絵文字などが前にあると、JavaScriptの文字列内の位置と、コードポイントを一つずつ数えた位置に差が出ます。エラーメッセージの『何文字目』を照合するときは、先頭を0と数えるか1と数えるかも含め、位置の単位をそろえます。

原因を一つに絞ってから、変換方針を決める

まず元の文字列を保存したまま、文字情報で差を特定します。次にNFCでそろうのか、互換変換が必要なのか、正規化では変わらない差なのかを分けます。さらに、変換してよい入力と原文を維持すべき入力を整理します。すべてをNFKCにすれば解決する、と決め打ちしない順序が重要です。

確認した例は、見た目の文章だけでなくコードポイントの並びも記録します。éとe+結合アクセント、全角A、ゼロ幅文字、別文字体系のaを試験へ残せば、『同じにしたいもの』と『区別したいもの』を同時に検証できます。問題の一件が通ったかだけでなく、区別すべき入力までまとめてしまっていないかを見ることが、安定した照合につながります。

検索で同じにすることと、元データを書き換えることは別

商品検索で全角Aを入力しても半角Aの商品を見つけたいなら、比較用の文字列だけを正規化する設計が考えられます。しかし、画面へ見せる正式な商品名まで自動的に書き換える必要があるとは限りません。元の表記と検索に使う表記を分ければ、見つけやすさを上げながら元データを保てます。このツールでNFCとNFKCを比べるのは、どの違いを同一視するかを見つけるためです。変更箇所が少ない方式ほど常に良い、あるいは変換できる文字が多いほど良い、という順位ではありません。

利用者ID、署名対象の文字列、パスワードのように一致規則が既に決まっているものへ、あとから正規化を足すと互換性が変わる可能性があります。見た目が同じだからといって、保存済みデータを書き換えてよい理由にはなりません。まず入力を文字情報ツールで調べ、次に正規化の各方式でどこが変わるかを見て、最後に対象システムの比較規則と照合します。この順序なら、『原因を発見した』ことと『運用データを変更してよい』ことを分けられます。

調査用の短い例には、合成済みと結合文字のé、半角と全角のA、ゼロ幅文字を挟んだ文字列など、原因の異なる組を一つずつ置きます。一度に全部を混ぜると、正規化で直ったのか、別の処理で消えたのかが分からなくなります。変換後の見た目だけでなくコードポイントとUTF-8のbyte数も確認し、想定した違いだけを変えたかを確かめます。似て見える文字をすべて同じにするという目標より、対象に必要な同一視の範囲を狭く定める方が、誤った一致を避けやすくなります。

参考資料

使うツール

使い方・解説 →