Unicode 正規化

同じ文字の異なる表現を統一する処理。NFC, NFD, NFKC, NFKD の 4 形式がある。

Unicode 正規化とは、同じ文字を表す異なるコードポイント列を統一的な形式に変換する処理です。Unicode では同一の文字を複数の方法で表現できる場合があり、たとえば「が」は 1 つのコードポイント U+304C (合成済み形式) でも、「か」(U+304B) +「゛」(U+3099、結合濁点) の 2 つのコードポイントでも表現できます。見た目は同じでもバイト列が異なるため、正規化なしでは文字列比較が正しく機能しません。

正規化には 4 つの形式があります。NFC (Canonical Decomposition followed by Canonical Composition) は分解してから再合成する形式で、Web 標準として推奨されています。NFD (Canonical Decomposition) は正準分解のみを行い、結合文字を分離した形式にします。NFKC (Compatibility Decomposition followed by Canonical Composition) は互換分解後に合成する形式で、全角英数字を半角に変換するなどの互換性変換を含みます。NFKD (Compatibility Decomposition) は互換分解のみを行います。

実務では正規化の選択がシステムの挙動に大きく影響します。検索エンジンやデータベースでは、ユーザーの入力と格納データの正規化形式が一致していないと、見た目が同じ文字列でも検索にヒットしない問題が発生します。たとえば、あるユーザーが NFC で入力した「が」と、別のユーザーが NFD で入力した「が」は、正規化なしでは別の文字列として扱われます。JavaScript では String.prototype.normalize() メソッドで任意の形式に変換でき、Python では unicodedata.normalize() 関数が利用できます。

macOS のファイル名については、世代によって扱いが異なります。旧 HFS+ は NFD に近い独自形式へ正規化した結果をディスクに保存していたため、macOS で作成したファイル名を Windows や Linux に持ち出すと比較が一致しない問題が起きました。一方、現行の APFS は与えられた形をそのまま保存し、照合時に正規化した形のハッシュを使って合成形と分解形を同一視します。Apple の公開資料では、macOS High Sierra 以降の APFS は正規化を区別しない (同じ名前の合成形と分解形を 1 つのディレクトリに併存させられない) と説明されています。したがって「macOS は NFD で保存する」という説明は旧 HFS+ 時代のものです。ただし、過去に分解形で作られたファイル名はその形のまま残るため、zip や tar で他の環境へ渡した時点で不一致が表面化します。Git には macOS 側の分解を打ち消す core.precomposeUnicode 設定があり、Linux や Windows とリポジトリを共有する場合に用います。

よくある誤解として、正規化は日本語や韓国語など特定の言語だけの問題と思われがちですが、実際にはラテン文字のアクセント記号 (é、ñ など) やアラビア文字の結合形式など、多くの言語で正規化が必要です。また、NFKC 正規化は全角英数字を半角に変換するため、意図せず文字の見た目が変わることがあり、検索用途では便利ですが表示用途では注意が必要です。丸囲み数字の「①」が「1」になり、「㈱」が「(株)」に展開されるのも NFKC です。

もう一つの落とし穴は、NFKC や NFKD ではなく NFC や NFD でも文字が置き換わる場合があることです。CJK 互換漢字は正準等価な統合漢字への対応が定義されているため、NFC をかけると別のコードポイントに変わります。Unicode のデータで数えると、互換漢字 1,014 字のうち 1,002 字が置き換えの対象です。たとえば人名で使われる U+FA10 の「塚」は NFC 後に U+585A の「塚」になります。互換漢字はもともと他の文字コードとの往復変換のために収録された文字であり、字形を指定する手段ではありません。特定の字形を保ちたい場合は、正規化に頼らず異体字セレクタを使います。

文字数カウントの観点では、正規化形式によって同じ文字のコードポイント数が変わるため、カウント結果に差が生じます。NFC では「が」は 1 コードポイントですが、NFD では 2 コードポイントになります。正確な文字数を把握するには、カウント前にテキストを統一的な正規化形式に変換しておくことが推奨されます。ただし NFC は「合成済みの文字が Unicode に用意されている組み合わせ」しかまとめられません。たとえば「カ」(U+30AB) に結合半濁点 (U+309A) を付けた文字は合成済み形が存在しないため、NFC をかけても 2 コードポイントのまま残ります。正規化だけで「見た目 1 文字 = 1 コードポイント」を保証できるわけではなく、見た目の単位で数えたい場合は書記素クラスタ単位の分割 (JavaScript では Intl.Segmenter) を併用します。

この記事を共有