文字数とバイト数の違い|UTF-8・Shift_JIS のカウント方法

約 10 分で読めます

プログラミングやデータベース設計において、「文字数」と「バイト数」の違いを正確に理解することは不可欠です。日本語のように 1 文字が複数バイトで表現される言語では、この違いを見落とすとデータの切り詰めや文字化けといった深刻な問題を引き起こします。この記事では、主要なエンコーディングにおける文字数とバイト数の関係を詳しく解説します。文字コードの入門書も併せて参考になります。文字数の確認には 文字数カウントス をご利用ください。

意外と知らない文字コードのトリビア

UTF-8 は 1992 年に Rob Pike と Ken Thompson によって設計されたとされています。興味深いのは、その設計がニュージャージー州のダイナーのランチョンマットの裏に描かれたスケッチから始まったという逸話です。当時は複数の Unicode エンコーディング案が競合しており、UTF-8 が採用された決定的な理由は「既存の C 言語のヌル終端文字列と互換性がある」という点でした。UTF-8 ではヌルバイト (0x00) が ASCII の NUL 以外に出現しないため、C の strlen()strcpy() がそのまま動作します。この特性がなければ、既存の膨大な C/UNIX ソフトウェア資産を書き換える必要が生じ、普及は大幅に遅れていたと考えられます。

もう 1 つ意外な事実として、日本語の「漢字」は UTF-8 で 3 バイトですが、一部の珍しい漢字 (CJK 統合漢字拡張 B 以降) は 4 バイトを消費します。たとえば「𠮷」(つちよし、吉の異体字) は 4 バイトです。居酒屋チェーン「𠮷野家」の「𠮷」をシステムで扱えないというトラブルは、この 4 バイト漢字の問題に起因するケースが多いとされています。技術的に補足すると、BMP (基本多言語面、U+0000〜U+FFFF) に収まる漢字は UTF-8 で 3 バイトですが、U+10000 以降の追加面に配置された漢字は 4 バイトになります。CJK 統合漢字拡張 B だけで約 42,000 字が登録されており、人名や地名に使われる異体字の多くがここに含まれます。

文字数とバイト数の基本的な違い

文字数はテキストに含まれる文字の個数を指し、バイト数はそのテキストをコンピュータが保存する際に必要なデータ量を指します。英語のアルファベットは 1 文字 = 1 バイトですが、日本語ではエンコーディングによって 1 文字あたりのバイト数が異なります。

エンコーディング 半角英数字 ひらがな・カタカナ 漢字 絵文字 なぜこのバイト数なのか
UTF-8 1 バイト 3 バイト 3 バイト 4 バイト ASCII 互換を維持しつつ全世界の文字を表現するための可変長設計。日本語が 3 バイトなのは Unicode のコードポイント範囲 (U+0800〜U+FFFF) に該当するため
Shift_JIS 1 バイト 2 バイト 2 バイト 非対応 日本語専用に設計されたため、日本語を 2 バイトで効率的に表現。ただし多言語には対応できない
EUC-JP 1 バイト 2 バイト 2 バイト 非対応 UNIX 環境向けに設計。Shift_JIS と同じ 2 バイトだが、バイト値の範囲が異なる
UTF-16 2 バイト 2 バイト 2 バイト 4 バイト 基本多言語面 (BMP) の文字を 2 バイトで表現。Java や JavaScript の内部表現に採用されている

たとえば「こんにちは」という 5 文字のテキストは、UTF-8 では 15 バイト、Shift_JIS では 10 バイトになります。同じ文字数でもエンコーディングによってバイト数が大きく異なる点に注意が必要です。

UTF-8 の仕組みと特徴

UTF-8 は現在の Web 標準エンコーディングであり、世界中の文字を扱えるユニバーサルな文字コードです。可変長エンコーディングを採用しており、文字によって 1〜4 バイトを使い分けます。この可変長設計の核心は、各バイトの先頭ビットパターンによって「このバイトが何バイト文字の何番目か」を一意に判定できる点にあります。

この設計の巧妙な点は「自己同期性」にあります。バイト列の途中から読み始めても、先頭ビットパターンを見るだけで文字の境界を特定できます。継続バイトは必ず 10 で始まるため、先頭バイトと区別が可能です。これにより、ファイルの途中からの読み取りや、破損したデータからの部分的な復元が容易になります。UTF-16 にはこの自己同期性がないため、バイト列の途中から読み始めるとサロゲートペアの前半と後半を誤認するリスクがあります。

UTF-8 の利点は ASCII との後方互換性にあります。英語のテキストはそのまま 1 バイトで表現されるため、既存の英語圏のシステムとの互換性が保たれます。加えて、UTF-8 はバイト列のソート順が Unicode のコードポイント順と一致するという特性も持ちます。これにより、バイト列の単純比較でも正しい辞書順ソートが実現でき、データベースのインデックスやファイルシステムの並び替えで有利に働きます。

Shift_JIS と EUC-JP の特徴

Shift_JIS と EUC-JP は、日本語専用のエンコーディングとして長年使われてきました。現在は UTF-8 への移行が進んでいますが、レガシーシステムやメール送信では依然として使用されるケースがあります。

Shift_JIS が「Shift」と名付けられた理由は、JIS X 0201 (半角カナ) の未使用領域にバイト値をシフト (ずらして) 配置したことに由来します。この設計により ASCII との共存が可能になりましたが、副作用として「5C 問題」が生じました。Shift_JIS の 2 バイト目に 0x5C (ASCII のバックスラッシュ「\」) が出現する漢字があり、C 言語のエスケープシーケンスやファイルパスの区切り文字と衝突します。「表」(0x955C)、「能」(0x945C)、「ソ」(0x835C) などが代表的な問題文字で、これらを含むファイル名やパスを処理する際にバグが発生するケースは現在でも報告されています。

EUC-JP は UNIX 環境で広く使われましたが、Shift_JIS とはバイト値の範囲が異なるため、同じ日本語テキストでもバイナリレベルでは全く異なるデータになります。EUC-JP の 2 バイト目は常に 0xA1〜0xFE の範囲に収まるため、Shift_JIS のような 5C 問題は発生しません。この安全性が UNIX 環境で EUC-JP が好まれた理由の 1 つです。

特徴 Shift_JIS EUC-JP
主な用途 Windows 環境、レガシーシステム UNIX/Linux 環境
日本語 1 文字のバイト数 2 バイト 2 バイト
絵文字対応 非対応 非対応
多言語対応 日本語のみ 日本語のみ
現在の推奨度 非推奨 (レガシー用途のみ) 非推奨 (レガシー用途のみ)

失敗パターン: 文字数とバイト数の混同で起きる事故

文字数とバイト数の違いを理解していないと、以下のような深刻なトラブルが発生します。

実務で注意すべきポイント

文字数とバイト数の違いは、以下のような実務場面で問題を引き起こすことがあります。

テキスト例 文字数 UTF-8 バイト数 Shift_JIS バイト数 バイト/文字比率 (UTF-8)
Hello 5 5 5 1.0
こんにちは 5 15 10 3.0
Hello こんにちは 11 21 16 1.9
café 4 5 非対応 1.25
𠮷野家 3 10 非対応 3.3
🎉🎊🎈 3 12 非対応 4.0
👨‍👩‍👧‍👦 (家族絵文字) 見た目 1 25 非対応

最後の「家族絵文字」は、4 つの絵文字が ZWJ (Zero Width Joiner、U+200D) で結合された合成文字です。見た目は 1 文字ですが、内部的には 7 つのコードポイント (4 つの絵文字 + 3 つの ZWJ) で構成されています。JavaScript の String.length は 11 を返し、[...str].length でも 7 を返します。「見た目の文字数」と「内部的な文字数」が大きく乖離する典型例であり、文字数カウントの実装で最も注意が必要なケースです。

プロのエンジニアが実践するテクニック

文字数とバイト数の問題に日常的に対処しているプロのエンジニアが実践しているテクニックを紹介します。

実例: 文字コードの選択ミスで起きた大規模障害

文字コードの問題は、時に大規模なシステム障害を引き起こします。以下は実際に起こりうるパターンです。

プログラミング言語ごとの文字数カウント

プログラミング言語によって、文字列の「長さ」が何を返すかは異なります。この違いを把握していないと、同じロジックを別の言語に移植した際にバグが混入します。

言語 長さ取得メソッド 返す値 「🎉」の長さ 「𠮷」の長さ 正確な文字数の取得方法
JavaScript .length UTF-16 コードユニット数 2 2 [...str].length
Python 3 len() コードポイント数 1 1 len(s.encode('utf-8')) でバイト数
Java .length() UTF-16 コードユニット数 2 2 .codePointCount(0, s.length())
Go len() バイト数 4 4 utf8.RuneCountInString()
Rust .len() バイト数 4 4 .chars().count()
Swift .count 書記素クラスタ数 1 1 .utf8.count でバイト数

注目すべきは Swift の設計です。Swift は文字列の .count が書記素クラスタ (grapheme cluster) 数を返すため、ZWJ 結合絵文字 (👨‍👩‍👧‍👦) も見た目どおり 1 としてカウントされます。これは「ユーザーが認識する文字数」に最も近い値を返す設計であり、他の言語とは根本的にアプローチが異なります。一方で、この設計のトレードオフとして、Swift の文字列は O(1) でのインデックスアクセスができず、先頭からの走査が必要になります。

Rust は文字列の扱いが最も厳密な言語の 1 つです。String 型は内部的に UTF-8 バイト列として保持されており、s[0] のようなインデックスアクセスはコンパイルエラーになります。これは「バイト単位のアクセスなのか文字単位のアクセスなのか」を開発者に明示的に選択させる設計思想に基づいています。

文字数とバイト数の違いを正しく理解することは、データの整合性を保ち、予期しないバグを防ぐための基本です。特に日本語を扱うシステムでは、エンコーディングの選択がシステム全体の品質を左右します。新規プロジェクトでは UTF-8 (データベースは utf8mb4) を標準とし、文字列の切り詰めは必ず文字数単位で行い、絵文字や異体字を含むテストケースを用意しておくことが、エンコーディング関連のトラブルを未然に防ぐ最善の方法です。テキストの文字数やバイト数を手軽に確認したい場合は、文字数カウントス をぜひご活用ください。