文字数の数え方とUTF-8 — 文字・バイト・書記素の違い

「この文章は何文字?」という素朴な問いには、実は一意な答えがありません。同じテキストでも、バイト数コードユニット数コードポイント数書記素(見た目の1文字)数のどれで数えるかによって結果が変わります。本記事では Unicode と UTF-8 の基礎から、プログラムでよく起きる「絵文字が2文字に数えられる」問題までを、検算した実例とともに整理します。

先に結論:人が見た目で数える「1文字」=書記素クラスタプログラムが内部で数える単位=コードユニット/コードポイント保存・送信に必要な量=バイト数。この3つは別物だと覚えておくと混乱しません。

1. 「文字数」は数え方で変わる

たとえば "a" は誰が数えても1文字・1バイトです。ところが "あ" は1文字ですが UTF-8 では3バイト、"😀" は見た目1文字なのに JavaScript の "😀".length2 を返します。これは間違いではなく、それぞれが異なる単位を数えているためです。まずはこの4つの数え方を区別することが出発点になります。

2. Unicode の基礎 — コードポイントと面

Unicode は世界中の文字に一意の番号を割り当てる規格で、その番号をコードポイントと呼び、U+ に続く16進数で表記します。たとえば "A"U+0041"あ"U+3042"😀"U+1F600 です。

コードポイントは U+0000 から U+10FFFF までの範囲を持ち、64Kごとに「面(plane)」で区切られます。

「BMPに収まるか、補助面か」は、後述する UTF-16 のサロゲートペアや str.length のズレに直結する重要な境界です。

3. UTF-8 — 可変長エンコーディング

UTF-8 はコードポイントを1〜4バイトの可変長でバイト列に変換する方式で、現在の Web の事実上の標準です。コードポイントの大きさに応じてバイト数が決まります。

コードポイント範囲UTF-8 のバイト数
U+0000U+007F1バイトASCII(A, 数字, 記号)
U+0080U+07FF2バイトラテン拡張、ギリシャ文字、キリル文字など
U+0800U+FFFF3バイトひらがな・漢字など日本語の多く(
U+10000U+10FFFF4バイト多くの絵文字(😀)、一部の漢字

UTF-8 の利点は、ASCII と後方互換であること(ASCII文字はそのまま1バイト)と、エンディアンに依存しないことです。一方で、1文字あたりのバイト数が一定でないため、「文字数 ≠ バイト数」が常に成り立ちます。

4. コードユニット vs コードポイント vs 書記素クラスタ

ここがプログラミングで最も誤解されやすい部分です。JavaScript の文字列は内部的に UTF-16 で表現され、str.lengthUTF-16 コードユニットの個数を返します。コードポイントや書記素の数とは限りません。

サロゲートペア(UTF-16 の補助面表現)

UTF-16 では BMP の文字を1コードユニット(2バイト)で表しますが、補助面(U+10000 以上)の文字は2つのコードユニットサロゲートペアで表します。だから "😀".length === 2 になります。コードポイント単位で数えたいときは、スプレッド構文や for...of を使います。

結合文字・ZWJ と書記素クラスタ

さらに、複数のコードポイントが結びついて1つの見た目になる場合があります。結合文字(例:ée + 結合アクセント U+0301 で表す)や、ZWJ(ゼロ幅接合子, U+200D)で絵文字を連結する家族絵文字などです。これらを「人が見た1文字」として数えるには Intl.Segmenter を使います。

// 書記素単位で数える
const seg = new Intl.Segmenter("ja", { granularity: "grapheme" });
[...seg.segment("😀")].length;          // 1
[...seg.segment("👨‍👩‍👧")].length; // 1
👨‍👩‍👧(家族絵文字)👨 + ZWJ + 👩 + ZWJ + 👧 という5つのコードポイント(UTF-16 では .length8)で構成されますが、見た目は1つの書記素です。SNS の文字数制限やバリデーションでは、どの単位で数えているかが体験を大きく左右します。

5. 実例で検算する

代表的な文字を、4つの数え方で並べてみます(UTF-8 のバイト数と JS の値は実際に検算済みです)。

文字列書記素コードポイントUTF-16 lengthUTF-8 バイト
a1111
1113
1113
😀1124
ée+U+0301)1223
👨‍👩‍👧15818

たとえば "あ"U+3042 で 3バイト範囲なので 1文字=3バイト。"😀"U+1F600 で補助面のため、書記素1・コードポイント1だが length は2、UTF-8 では4バイトです。"👨‍👩‍👧" は絵文字3つ+ZWJ2つの計5コードポイント。各絵文字が4バイト×3=12、ZWJ(U+200D)が3バイト×2=6で、合計18バイトとなります。

6. 改行・空白とバイト数が必要な場面

実務では「何を1文字と数えるか」だけでなく、空白や改行をどう扱うかも問題になります。

一方、バイト数が必要になる場面もはっきりしています。

場面数えるべき単位理由
入力欄の文字数制限(UI)書記素人の「見た目の1文字」と一致させるため
DBのカラム長(VARCHAR 等)バイトまたは文字DB・文字セットの定義により上限の単位が異なる
通信量・ファイルサイズバイト実際に転送・保存される量だから
暗号・ハッシュの入力バイト処理対象は常にバイト列だから
Free Tool 文字数カウントで実際に数える 文字数・バイト数などをその場で計測できます。テキストを貼り付けて、数え方による違いを確かめてみてください。

よくある質問(FAQ)

なぜ絵文字が2文字と数えられることがあるのですか?

JavaScript の str.length は UTF-16 のコードユニット数を返します。「😀」のような補助面(U+10000 以上)の文字は 2 つのコードユニット(サロゲートペア)で表されるため、length では 2 と数えられます。人が見た目で数える文字数(書記素)と一致させたい場合は [...str].length や Intl.Segmenter を使います。

文字数とバイト数はどう違いますか?

文字数は「いくつの文字があるか」、バイト数は「保存・送信に何バイト必要か」です。UTF-8 では ASCII は 1 バイト、日本語の多くは 3 バイト、絵文字は 4 バイトになるため、同じ 1 文字でもバイト数は異なります。データベースの桁数設計や通信量の見積もりではバイト数が重要です。

書記素クラスタとは何ですか?

人が「1 文字」として知覚する単位のことです。結合文字や ZWJ(ゼロ幅接合子)によって複数のコードポイントが 1 つの見た目になることがあり、例えば「👨‍👩‍👧」は複数のコードポイントから成りますが書記素としては 1 つです。JavaScript では Intl.Segmenter で書記素単位に分割できます。

← 技術ブログ一覧へ戻る