サロゲートペアとは — UTF-16の仕組み、絵文字や文字列処理で起きる問題と実務的な対処法
サロゲートペアとは — 概要
サロゲートペア(surrogate pair)は、Unicode の符号位置(コードポイント)が U+10000 から U+10FFFF の範囲にある「補助平面(supplementary planes)」の文字を、UTF-16 の 16 ビット単位(コードユニット)で表現するために用いられる 2 つ組のコードユニットです。UTF-16 は 16 ビット単位を基本とするため、16 ビットだけでは 0x0000〜0xFFFF(BMP: Basic Multilingual Plane)までしか直接表現できません。その上位の領域を扱うために考案されたのがサロゲートペアです。
なぜサロゲートペアが必要になったか(歴史的背景)
最初期の Unicode / UCS-2 の設計は 16 ビット(0〜65535 = 0x0000〜0xFFFF)で多言語文字を扱うという前提でした。しかし、文字や絵文字、歴史文字、追加の漢字などの需要が増え、65536 個以上の符号位置が必要になりました。これに対応するため、16 ビット単位を拡張して 21 ビットまで扱える UTF-16 エンコーディングが導入され、サロゲートペアがその仕組みとして採用されました。
サロゲートペアの構造と計算方法
- 高位サロゲート(High Surrogate): U+D800 〜 U+DBFF(16 ビット値 0xD800〜0xDBFF)
- 低位サロゲート(Low Surrogate): U+DC00 〜 U+DFFF(16 ビット値 0xDC00〜0xDFFF)
サロゲートペア H(高位)と L(低位)から元の Unicode コードポイント U を復元する計算式は次の通りです。
U = ((H − 0xD800) << 10) + (L − 0xDC00) + 0x10000逆に、U(U+10000〜U+10FFFF)からサロゲートペア H, L を得る式は:
U' = U − 0x10000
H = 0xD800 + (U' >> 10)
L = 0xDC00 + (U' & 0x3FF)
具体例:絵文字「😀」は U+1F600。計算すると高位サロゲートは 0xD83D、低位サロゲートは 0xDE00 となります。UTF-16 表現は 0xD83D 0xDE00(16 ビット単位で二つ)です。
UTF-8・UTF-32 とサロゲートの違い
サロゲートペアは UTF-16 特有の概念です。UTF-8 や UTF-32 ではサロゲートペアという表現は存在しません。
- UTF-8: 任意の Unicode コードポイントを可変長(1〜4 バイト)で表し、補助平面も 4 バイトで直接表現するためサロゲート概念は不要。ただし、UTF-8 にサロゲートコードポイント(U+D800〜U+DFFF)をエンコードすることは不正(非正規)と見なされます。
- UTF-32: 各コードポイントを 32 ビットで直接表すため、もちろんサロゲートは不要。各値がそのままコードポイントになります。
実務で遭遇する問題点(落とし穴)
サロゲートペアを理解していないと、文字列処理で以下のような問題が発生します。
- 文字数の誤認: JavaScript や Java、C# などの多くの環境では String.length(あるいは類似の API)が「コードユニット(16 ビット単位)」の数を返します。補助平面の文字は 2 コードユニットになるため、見た目の文字数と一致しません(例: "😀".length は 2)。
- 文字列操作の不整合: 文字列の切り出しやインデックス参照でサロゲートの半分を切り出してしまうと、不正な(未対応の)サロゲートや表示崩れが起きます。
- 正規表現の扱い: JavaScript の正規表現では u フラグを付けないとサロゲートペアを正しく扱えない、などの挙動があります。例えば /\u{1F600}/u は補助平面の文字を正しくマッチしますが、u フラグなしだと期待と違う動きをすることがあります。
- 言語実装の差: Python(古い「narrow build」時代)や他の環境では内部表現が UTF-16 相当だったため、len() やインデックス操作がコードユニット単位になり問題が起きることがありました。Python は 3.3(PEP 393)以降で内部表現が改善され、一般的にはコードポイント単位で扱われますが、過去のバージョンや互換性を考慮する必要があります。
プログラミング言語別の取り扱いと対処法
- JavaScript: 文字列は UTF-16 のコードユニット列。length はコードユニット長。補助平面を正しく扱うには String.prototype.codePointAt、String.fromCodePoint を利用、もしくは for...of での反復(これはコードポイント単位で反復)や正規表現の u フラグを用いる。
- Java: java.lang.String は UTF-16 で内部的にコードユニットを保持。length() はコードユニット長。Character.codePointAt、Character.toChars、String.codePoints() などの API を使ってコードポイント単位での処理を行う。
- C#/.NET: System.String は UTF-16。Length はコードユニット長。.NET Core 3.0 以降は System.Text.Rune 型が導入され、Unicode スカラー値(コードポイント)を扱いやすくなった。
- Python: Python 3.3 以降は PEP 393 により内部表現が柔軟化され、通常はコードポイント単位で振る舞う。過去の narrow ビルド(極めて古い環境)ではサロゲートペア問題を考慮する必要があったが、現在の標準的な Python ではあまり気にする必要はない。ただし外部バイナリやエンコーディング変換時は注意。
- データベースや古い API: 古い DB やミドルウェアが UTF-16 相当の内部表現を期待していたり、補助平面をサポートしていない場合、絵文字が切れたり格納できなかったりすることがある。
未対応・不正な(非整合な)サロゲートとセキュリティ
サロゲート範囲(U+D800〜U+DFFF)のコードポイント自体は Unicode の「割当対象外」領域であり、正当な Unicode スカラー値としては使用されません。UTF-8 エンコーディングではこれらをエンコードすることは不正とされます。一方、実行環境によっては不正な(単独の)サロゲートコードユニットを含む文字列を内部に保持できる場合があり、バリデーションや正規化の際に予期しない振る舞い(例: 回避攻撃、正規化の差異による認証エラー等)が生じる可能性があります。入力の正規化・検証はセキュリティ上重要です。
実務的なベストプラクティス
- 文字数判定や切り出しは「ユーザーが見ている文字(グラフェムクラスタ)」や「Unicode スカラー値(コードポイント)」のどちらで扱うか要件を明確にする。多くの場合はグラフェムクラスタ単位のカウントがユーザー期待に合う(例: 国際化された絵文字は複数のコードポイントを結合して 1 文字に見える)。
- 言語が提供するコードポイント単位の API(例: JavaScript の codePointAt/fromCodePoint、String.prototype[@@iterator]、Java の codePoints()、.NET の Rune 等)を活用する。
- 正規表現で補助平面を扱う場合は(言語がサポートするなら)Unicode フラグやコードポイント表現を利用する。例: JavaScript の /\u{1F600}/u。
- 外部入出力(DB、ファイル、ネットワーク)はエンコーディングを明示し、UTF-8 を推奨する。UTF-8 は補助平面を問題なく表現でき、互換性が高い。
- サニタイズや正規化は Unicode 標準(Normalization Form)とセキュリティ方針に従って行う。予期しないサロゲートや未割当コードが入っていないか検査する。
- テストデータに補助平面の文字(絵文字、古い漢字など)を含めてユニットテストを行い、文字列操作が破綻しないことを確認する。
デバッグとツール
文字列に含まれるサロゲートやコードポイントを調べるには、以下のような方法が有効です。
- 16 進でコードユニット列を表示(例: バイナリエディタやデバッグログで 0xD83D 0xDE00 を確認)
- 言語/ライブラリが提供する code point 関数でコードポイント列を列挙
- ICU(International Components for Unicode)などのライブラリでグラフェムクラスタや正規化を確認
まとめ
サロゲートペアは、UTF-16 において 16 ビット単位では表せない補助平面の文字を扱うための仕組みです。見た目上 1 文字でも内部的には 2 個のコードユニットになるため、文字列長の計算、切り出し、正規表現、データ保存など多くの場面で注意が必要です。近年は UTF-8 が広く使われ、言語実装側でもコードポイント/グラフェムクラスタ単位の API が充実してきていますが、既存システムやバイナリインターフェースでは未だサロゲート関連の不整合が問題を引き起こすことがあります。要件に合わせて「コードユニット」「コードポイント」「グラフェムクラスタ」のどれで処理するかを明確にし、適切な API とテストを用いることが重要です。
参考文献
- Unicode Consortium — Unicode FAQ(UTF/ BOM 等)
- The Unicode Standard — Chapter 3: UTF-16(サロゲートの説明)
- Unicode Consortium — Surrogates(サロゲートに関する FAQ)
- MDN — String.prototype.codePointAt()
- MDN — 正規表現と Unicode(u フラグなど)
- PEP 393 — Flexible String Representation(Python の文字列内部表現)
- Microsoft Docs — Unicode and Rune(.NET の Rune)
- Unicode Standard Annex #29 — Text Segmentation(グラフェムクラスタと分割)


