「😀」を入力したら文字数が2になった。「𠮷野家」の「𠮷」が正しく表示されない。こうした経験はないでしょうか。これらの問題の原因は「サロゲートペア」と呼ばれる文字の内部表現にあります。本記事では、サロゲートペアの仕組みから、プログラミングやデータベースでの実務的な対処法まで、具体例を交えてわかりやすく解説します。
1. サロゲートペアとは何か
サロゲートペア(Surrogate Pair)とは、Unicodeの文字をUTF-16で表現する際に、1つの文字を2つの16ビットコードユニット(合計4バイト)で表す仕組みのことです。通常のUTF-16では、1文字を1つの16ビット(2バイト)で表現しますが、Unicodeに収録される文字が65,536文字を超えたため、追加の文字を表現するためにこの仕組みが導入されました。
具体的には、U+10000からU+10FFFFまでの範囲にある文字(補助文字、Supplementary Characters)は、「上位サロゲート(U+D800〜U+DBFF)」と「下位サロゲート(U+DC00〜U+DFFF)」の2つのコードユニットを組み合わせて表現されます。この2つのペアで初めて1つの文字を表すため、「サロゲートペア」と呼ばれています。
2. なぜ文字数カウントが狂うのか
JavaScriptやJavaなど、内部的にUTF-16を使用するプログラミング言語では、文字列の.lengthプロパティはコードユニットの数を返します。つまり、サロゲートペアで表現される文字は、見た目は1文字でも.lengthでは2としてカウントされてしまうのです。
■ 具体例で見る文字数のズレ
| 文字 | Unicode | 見た目 | .length | 正しい文字数 |
|---|---|---|---|---|
| あ | U+3042 | 1文字 | 1 | 1 |
| 😀 | U+1F600 | 1文字 | 2 | 1 |
| 𠮷 | U+20BB7 | 1文字 | 2 | 1 |
| 🇯🇵 | U+1F1EF U+1F1F5 | 1文字 | 4 | 1 |
| 👨👩👧👦 | 複数コードポイント結合 | 1文字 | 11 | 1 |
特に注目すべきは、国旗絵文字や家族絵文字のように、複数のコードポイントがZWJ(Zero Width Joiner)で結合された絵文字です。これらは見た目は1文字ですが、内部的には非常に多くのコードユニットで構成されています。
3. サロゲートペアに該当する文字の種類
サロゲートペアが必要になる文字は、日常的に使われるものも少なくありません。以下に代表的なカテゴリを紹介します。
- 絵文字(Emoji):😀、🎉、🔥、❤️🔥 など。スマートフォンの普及により、ビジネスメールやSNSでも頻繁に使われるようになりました。
- CJK統合漢字拡張B以降:𠮷(つちよし)、𩸽(ほっけ)、𠀋(じゅう)など。人名や地名に使われる旧字体・異体字が多く含まれます。
- 音楽記号:𝄞(ト音記号)、𝄡(ハ音記号)など。楽譜関連のテキストで使用されます。
- 数学記号:𝕏、𝔸 など。数学や論理学の文書で使われる特殊な書体の文字です。
- 古代文字:エジプトのヒエログリフ、楔形文字など。学術研究で使用されます。
4. プログラミング言語別の対処法
サロゲートペアによる文字数のズレを正しく処理するための方法を、主要なプログラミング言語別に紹介します。
■ JavaScript
JavaScriptでは、Array.from()やスプレッド構文を使うことで、サロゲートペアを正しく1文字として扱えます。
// NG: サロゲートペアが2文字になる
"😀".length; // → 2
"𠮷野家".length; // → 4
// OK: 正しい文字数を取得
[..."😀"].length; // → 1
Array.from("𠮷野家").length; // → 3
// OK: 正規表現のuフラグを使う
"😀test".match(/./gu).length; // → 5
■ Java
JavaではString.length()がUTF-16コードユニット数を返すため、codePointCount()メソッドを使用します。
String text = "😀𠮷野家";
text.length(); // → 6(NG)
text.codePointCount(0, text.length()); // → 4(OK)
■ Python
Python 3は内部的にUnicodeを正しく扱うため、len()でサロゲートペア文字も1文字としてカウントされます。ただし、結合文字列(家族絵文字など)は複数コードポイントとしてカウントされる点に注意が必要です。
len("😀") # → 1(OK)
len("𠮷野家") # → 3(OK)
len("👨👩👧👦") # → 7(結合文字列のため)
■ SQL(MySQL)
MySQLでは、文字セットにutf8(3バイトまで)を使用していると、サロゲートペア文字を保存できません。utf8mb4を使用する必要があります。
-- NG: utf8では絵文字が保存できない
ALTER TABLE posts CHARACTER SET utf8;
-- OK: utf8mb4なら絵文字も保存可能
ALTER TABLE posts CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
5. データベースでの注意点
サロゲートペア文字をデータベースに保存する際は、以下の点に特に注意が必要です。
- 文字セットの確認:MySQLの
utf8は最大3バイトまでしか対応していないため、絵文字やCJK拡張漢字を保存するにはutf8mb4が必須です。PostgreSQLやSQLiteはデフォルトでUTF-8の全範囲に対応しています。 - カラムの文字数制限:
VARCHAR(10)と指定した場合、サロゲートペア文字が含まれると、見た目の文字数と実際に格納できる文字数が異なる場合があります。 - インデックスのキー長:
utf8mb4では1文字あたり最大4バイトを消費するため、インデックスのキー長制限(InnoDB: 767バイト)に引っかかりやすくなります。 - バックアップとリストア:文字セットの設定が異なる環境間でデータを移行すると、サロゲートペア文字が欠落したり文字化けしたりする可能性があります。
6. 絵文字の文字数カウントが複雑な理由
絵文字の文字数カウントは、サロゲートペアだけでなく、以下のような追加の複雑さがあります。
- 異体字セレクタ(Variation Selector):❤️のように、基本文字(❤ U+2764)に異体字セレクタ(U+FE0F)が付加されて絵文字として表示されるケースがあります。
- ZWJ結合(Zero Width Joiner):👨💻のように、複数の絵文字がZWJ(U+200D)で結合されて1つの絵文字として表示されます。
- 国旗絵文字:🇯🇵のように、2つのRegional Indicator文字の組み合わせで国旗を表現します。各Regional Indicatorがサロゲートペアなので、1つの国旗で4つのコードユニットを消費します。
- 肌の色修飾子:👋🏽のように、基本の絵文字に肌の色を表す修飾子(U+1F3FB〜U+1F3FF)が付加されます。
このような複雑さから、「見た目の1文字」を正確にカウントするには、Unicodeの書記素クラスタ(Grapheme Cluster)という単位で分割する必要があります。JavaScriptではIntl.Segmenter APIを使うことで、書記素クラスタ単位での正確なカウントが可能です。
// Intl.Segmenter で書記素クラスタ単位のカウント
const segmenter = new Intl.Segmenter("ja", { granularity: "grapheme" });
const count = [...segmenter.segment("👨👩👧👦🇯🇵")].length;
// → 2(家族絵文字1つ + 国旗1つ)
7. mojisucount.comでのサロゲートペア検出
当サイトの文字数カウントツールでは、入力テキストに含まれるサロゲートペア文字の数をリアルタイムで検出・表示しています。「読了時間・その他」セクションの「サロゲートペア」の項目で確認できます。
また、バイト数の表示では、UTF-8・UTF-16・Shift_JIS・EUC-JPの各エンコーディングでのバイト数を同時に確認できるため、サロゲートペア文字がデータサイズに与える影響を一目で把握できます。フォームの文字数制限やデータベースのカラムサイズを設計する際に、ぜひご活用ください。
8. 実務でよくあるトラブルと対策
■ トラブル1:ユーザー名に絵文字を入れたら登録できない
原因はデータベースの文字セットがutf8(3バイト制限)になっていることがほとんどです。utf8mb4に変更し、接続時の文字セットもutf8mb4に設定することで解決します。
■ トラブル2:文字数バリデーションで弾かれる
「10文字以内」というバリデーションで、絵文字を含むテキストが不正に弾かれるケースです。.lengthではなく、Array.from(text).lengthやIntl.Segmenterを使って正しい文字数を取得するようにしましょう。
■ トラブル3:CSVエクスポートで文字化け
Shift_JISでCSVを出力すると、サロゲートペア文字は表現できないため文字化けします。絵文字を含むデータをCSVで扱う場合は、UTF-8(BOM付き)で出力する必要があります。
9. まとめ
- ✓ サロゲートペアとは、UTF-16で1文字を4バイト(2つのコードユニット)で表現する仕組み。
-
✓
絵文字・旧字体・特殊記号がサロゲートペアに該当し、
.lengthで正しくカウントできない。 -
✓
JavaScriptでは
Array.from()やスプレッド構文、Intl.Segmenterで正確にカウント可能。 -
✓
MySQLでは
utf8mb4を使用しないと絵文字を保存できない。 - ✓ mojisucount.comではサロゲートペア数をリアルタイムで検出・表示している。