>_devtools-hub
テキストエンコード開発tips

文字数が合わない原因 — 絵文字・結合文字・サロゲートペアの数え方

画面の文字数カウントとサーバー側の判定が食い違う原因を整理します。UTF-16のコードユニット、コードポイント、書記素クラスタという3つの数え方の違いと、絵文字や結合文字が混ざったときの挙動、バイト数で決まる制限の見分け方をまとめます。

入力欄では100文字以内と表示されているのに、保存しようとするとサーバーに弾かれる。あるいは、名前が途中で切れて表示される。こうした食い違いの原因は、たいてい「1文字」の定義がそろっていないことにあります。プログラムにとっての1文字は1通りではなく、少なくとも3通りの数え方があり、絵文字や濁点付きの文字が混ざると結果がはっきり分かれます。さらに、文字数ではなくバイト数で制限している場所も混在しています。この記事では、3つの数え方の違いと、実際に何が入っているかを確かめる手順、そして数え方をそろえる考え方を紹介します。

1. 「1文字」の数え方は3通りある

まず押さえるのが、数え方が3層あることです。1つ目がUTF-16のコードユニットで、JavaScriptの length が返すのはこれです。2つ目がコードポイントで、Unicodeが文字に割り当てた番号の個数です。3つ目が書記素クラスタで、人間が見て1文字と感じるまとまりです。普通の日本語や英数字では3つとも同じ値になりますが、絵文字が入ると分かれます。たとえば家族を表す絵文字は、人物の絵文字3つをゼロ幅接合子でつないだもので、コードユニットでは8、コードポイントでは5、書記素では1と数えられます。もう1つの例が濁点付きのかなで、1つの文字として登録されたものと、清音に結合用の濁点を足したものがあり、見た目は同じでもコードポイントの数が1と2で変わります。文字数カウンターにテキストを貼り付けると、この3つの数え方が並べて表示されるので、手元の文字列がどこで分かれるかをその場で確かめられます。どれが正しいということはなく、画面の表示に合わせたいなら書記素、保存領域を見たいならバイト数、というように目的で選びます。

この手順で使うツール

文字数カウンター

文字・単語・バイト数をリアルタイム集計

2. 実際に何が入っているかを1文字ずつ見る

数え方の違いを理解したら、次は問題のテキストに実際に何が入っているかを確認します。ここを飛ばして推測で直すと、見た目が同じ別の文字が残ったままになります。String Inspector に文字列を貼り付けると、1文字ずつコードポイントとUTF-8のバイト数、そして文字種のカテゴリが色分けで表示されます。見るべきは、想定していない文字が紛れていないかです。ゼロ幅接合子や異体字セレクタのように画面上に姿を持たない文字、全角と半角で見分けのつきにくい記号、コピー元から持ち込まれた特殊な空白などがここで露見します。文字数が1つ多い、という程度のずれは、ほとんどがこうした見えない文字です。合計の文字数とバイト数も同時に出るので、画面のカウンターの値とどこで食い違っているかを突き合わせられます。

この手順で使うツール

String Inspector

文字列を1文字ずつ分解 — Unicodeコードポイント・UTF-8バイト数・文字種を可視化

3. 文字数で決まる制限とバイト数で決まる制限を見分ける

制限に引っかかる側の単位も確認が必要です。入力欄のバリデーションは文字数で書かれていることが多い一方、データベースの列やHTTPヘッダー、ファイル名の上限などはバイト数で決まっていることがあります。UTF-8では、英数字は1バイト、日本語の多くは3バイト、絵文字は4バイトを使います。つまり同じ100文字でも、英字だけなら100バイト、日本語なら300バイト前後、絵文字が混ざればさらに増えます。文字数カウンターにテキストを貼り付けると、文字数と並べてUTF-8のバイト数が表示され、全角と半角の内訳も確認できます。保存に失敗する境目がバイト数側にあるのか文字数側にあるのかは、この2つの数字を並べれば判断できます。見積もるときは、日本語は1文字3バイトとして計算しておくと安全側に倒せます。

この手順で使うツール

文字数カウンター

文字・単語・バイト数をリアルタイム集計

4. 紛れ込んだ文字の正体を特定する

見えない文字が原因だと分かったら、それが何なのかを特定します。よく出てくるのが、絵文字の連結に使われるゼロ幅接合子、絵文字の見た目を指定する異体字セレクタ、コピー時に紛れ込むゼロ幅スペース、そして半角スペースに似た各種の空白文字です。Unicode Browser では、コードポイントや文字名で検索して、その文字のUTF-8とUTF-16のバイト列、HTMLエンティティ、JavaScriptのエスケープ表記などをまとめて確認できます。String Inspector で見つけたコードポイントをここで引けば、何者で、どう表記すれば取り除けるかが分かります。除去のルールを書くときは、見た目で判断せずコードポイントで指定するのが確実です。ゼロ幅スペースだけを消したいのに空白全般を消してしまい、必要な区切りまで落ちる、という事故を避けられます。

この手順で使うツール

Unicode Browser

Unicode文字を名前・コードポイントで検索・ブロック別ブラウジング

5. 入力側と保存側で数え方をそろえる

最後に、仕組みとして食い違いを起こさない形に整えます。まず決めるのは、利用者に見せる上限をどの単位で表すかです。名前やコメントのように人が読む項目は、書記素で数えるのが体感と合います。JavaScriptでは Intl.Segmenter を使うと書記素単位で分割でき、絵文字を1文字として数えられます。次に、入力側のチェックと保存側のチェックを同じ定義で書くことです。画面は文字数、データベースはバイト数、という組み合わせのまま運用すると、境目の値だけで失敗する不具合が残り続けます。どちらかにそろえられない場合は、画面側の上限を保存側の上限より十分に小さく設定して、境目に当たらないようにします。あわせて、保存前に正規化を一度かけておくと、濁点付きのかなのように複数の表現を持つ文字が片方の形に統一され、比較や検索のずれも減らせます。

まとめ

文字数が合わない問題は「3つの数え方のどれを使っているか」と「実際に何の文字が入っているか」を分けて見ると整理できます。絵文字が混ざればコードユニット、コードポイント、書記素の値は必ず分かれますし、保存に失敗する境目はバイト数側にあることが多いです。入力側と保存側の定義をそろえ、保存前に正規化をかけておく。この2つを決めておくだけで、原因不明の文字数ずれはかなり減らせます。

このツールで試す

Text Case Converter

テキストを8形式のケースに一括変換・コピー

開発者向けテキストユーティリティ

使ってみる →

BOOTH

文字数カウンター

文字・単語・バイト数をリアルタイム集計

テキスト執筆開発者向け

使ってみる →

BOOTH

String Inspector

文字列を1文字ずつ分解 — Unicodeコードポイント・UTF-8バイト数・文字種を可視化

テキストUnicodeエンコード開発者向け

使ってみる →

BOOTH

Unicode Browser

Unicode文字を名前・コードポイントで検索・ブロック別ブラウジング

開発者向けテキストエンコード

使ってみる →

BOOTH

関連ガイド