結合文字(Unicode)完全ガイド:正規化・グラフェム・実務対応とセキュリティ対策
結合文字とは — 基本の定義
結合文字(けつごうもじ、英: combining character)は、Unicodeで定義される文字カテゴリの一つで、前後の「基底文字(base character)」に視覚的に結合して表示される文字です。代表的にはアクセント記号(アキュート、グレーブ、サーカムフレックスなど)やダイアクリティカルマーク、上付き・下付きの記号、音声記号などが該当します。たとえば「é」は単一のプリコンポーズド文字 U+00E9 でも表現できますが、アルファベットの「e」(U+0065)に「COMBINING ACUTE ACCENT」(U+0301)を付けた「e + ◌́」(U+0065 U+0301)という組合せでも表現できます。後者が結合文字を用いた表現です。
Unicode上の位置づけとブロック
結合文字は複数のUnicodeブロックに分散しています。主要なブロック例を挙げると:
- Combining Diacritical Marks(U+0300–U+036F)
- Combining Diacritical Marks Supplement(U+1DC0–U+1DFF)
- Combining Diacritical Marks for Symbols(U+20D0–U+20FF)
- Combining Half Marks(U+FE20–U+FE2F)
これらのコードポイントは、Unicodeの規格上「結合する」という振る舞い(表示上の合成)を想定していますが、最終的な見た目はフォントと描画エンジン(レンダラ)に依存します。
結合文字と正規化(Normalization)
Unicodeには同じ見た目を別のコード列で表現できる(前述の「é」の例など)ことから、テキスト比較や保存の際に問題が生じます。これを扱うために「正規化(Normalization)」という概念があります。代表的な正規化形式は次の4つです。
- NFC(Normalization Form C): 可能な限り合成(compose)してプリコンポーズド表現にする。
- NFD(Normalization Form D): 可能な限り分解(decompose)して基底文字+結合文字の列にする。
- NFKC / NFKD: 上記の互換分解を含む(互換性のある文字を正規化するため字体や幅の違いも平坦化する)。
実務では、比較や検索の前に入力を正規化して一貫した内部表現に揃えることが推奨されます(例:NFCに統一)。ただし、ファイル名やプラットフォーム固有の挙動を考慮する必要があります(後述)。
グラフェム(文字の単位)と可視長
見た目上の「1文字」は、必ずしも1つのUnicodeコードポイントとは限りません。例えば、1つの基底文字に複数の結合文字が重なる場合や、ゼロ幅結合子(ZWJ)を使った合成(主に絵文字の合成)などにより、複数のコードポイントが1つのユーザー視点の文字(グラフェムクラスター)になります。Unicodeはグラフェムクラスタ分割のアルゴリズム(Unicode TR29)を定めており、ユーザー向けの長さカウントや文字列切り取り(substring)にはこれを考慮する必要があります。
ゼロ幅結合子(ZWJ)・ゼロ幅非結合子(ZWNJ)と絵文字の合成
U+200D(ZERO WIDTH JOINER, ZWJ)やU+200C(ZERO WIDTH NON-JOINER, ZWNJ)といったゼロ幅文字は、結合や非結合の挙動を制御します。ZWJは隣接する文字を結合して一つの合成形(特に絵文字の家族、職業や肌色の組合せ)を生成するために使われます。また絵文字の表示形式(テキスト表示か絵文字表示か)を切り替えるためにバリエーションセレクタ(VS-15/VS-16)と併用されることがあります。これらもグラフェムクラスタの一部として扱う必要があります。
実際に起きる問題と落とし穴
- 比較の不一致: 見た目は同じでもコード列が異なると等価判定がfalseになる("é"(U+00E9)と "e+U+0301")。
- 文字数カウントの誤り: JavaScriptや多くの言語でのString.lengthやlen()はコードポイント(あるいはUTF-16単位)を返し、ユーザーの見た目単位(グラフェム)と異なる。
- 切り取りや正規表現の誤動作: 結合文字の途中で切ると不正な表示になったり、無効な文字列ができる。
- ファイル名の不一致: OSやファイルシステムによって正規化の扱いが異なる(macOSはファイル名を分解正規化NFDで保存する挙動がある一方、Windowsは通常ユーザーが入力したまま保存する)。
- セキュリティ問題: 見た目は同じでも別のコード列を使ってフィッシングやスプーフィングが可能(Unicodeのセキュリティ勧告に注意)。
各言語・環境での対処法(実用ガイド)
- 正規化: 入出力やDB保存の前にUnicode正規化を行う(一般的にはNFC推奨)。主要言語はAPIを提供している(例: Python: unicodedata.normalize、Java: java.text.Normalizer、JavaScript: String.prototype.normalize)。
- 長さ・切り取り: ユーザー向けの長さやトリムはグラフェムクラスタベースで行う。ICUやlibgrapheme、またはPCREの\X(環境による)を使うと良い。
- 正規表現: JavaScriptの標準正規表現は\Xをサポートしていないため、Intl.Segmenterや外部ライブラリで区切る。PCREやICUベースのエンジンは\XやUnicodeプロパティを利用できることがある。
- ファイル名: ファイルシステム固有の挙動を理解する(macOSではNFD、Windowsは入力保持、Linux系はバイト列をそのまま扱う)。ファイル名比較を行う場合は相手のファイルシステムに合わせて正規化を行うか、正規化の有無を意識する。
- IDN(国際化ドメイン名)やURL: Punycode/IDNA処理を必ず行い、Unicodeの見た目の類似問題に関する安全策(UTS #39等)を導入する。
結合文字とフォント・レンダリング
結合文字はフォントに依存します。適切な合字や合成Glyphがフォントに含まれていれば美しく配置されますが、無い場合は結合文字が基底文字の上や下に重なるなど不格好な表示になったり、まったく期待した位置に配置されないことがあります。Webやアプリで多言語を扱う場合は、対象言語に対応したフォントを用意し、描画エンジン(Harfbuzz, Uniscribe, CoreText など)が必要な処理をできるか確認することが重要です。
セキュリティ上の注意点
Unicodeの柔軟性は同時にセキュリティリスクを伴います。異なるコード列で見た目が等しい「同形(homoglyph)」や、文字列操作の不整合を狙った攻撃が存在します。対策としては、期待する文字集合をホワイトリストで制限したり、正規化の一貫適用、IDNAやUnicodeセキュリティ勧告(UTS #39)に従うことなどが挙げられます。
実務でのベストプラクティス(チェックリスト)
- ユーザー入力は可能なら受領時に正規化(プロトコルや環境に合わせてNFCなど)して保存する。
- DBや索引は正規化済みの値で比較・検索を行う。検索時に正規化の有無でヒット漏れが出ないように注意する。
- UIでの文字数カウントやトリミングはグラフェムクラスタ単位で行う。
- 外部システムとインターフェースする場合は、どの正規化を期待するかを契約で明確化する。
- ファイル名・URL・ドメイン名は環境依存性を踏まえた処理を行い、必要に応じて正規化や検証を行う。
- セキュリティ方針を整備し、同形文字攻撃や混在スクリプト問題に対処する。
まとめ
結合文字は多くの言語や記号表現を柔軟に扱える一方で、正規化・表示・操作・セキュリティの各面で注意が必要です。ユーザーにとって「見た目の1文字」を正しく扱うためには、Unicodeの正規化やグラフェムクラスタの概念、環境ごとのファイルシステムの挙動、そして適切なライブラリ・レンダラを理解して利用することが不可欠です。
参考文献
- Unicode Standard Annex #15: Unicode Normalization Forms (TR15)
- Unicode Standard Annex #29: Unicode Text Segmentation (TR29)
- Unicode Security Mechanisms (UTS #39)
- Unicode Code Charts — 各種 Combining Marks のチャート(例: U+0300–U+036F)
- ICU (International Components for Unicode) — ライブラリとドキュメント
- Python: unicodedata.normalize — ドキュメント
- MDN: String.prototype.normalize()
- Wikipedia: Combining character(概説)


