文字数とバイト数の違い|UTF-8・Shift_JIS のカウント方法
プログラミングやデータベース設計において、「文字数」と「バイト数」の違いを正確に理解することは不可欠です。日本語のように 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 バイトを使い分けます。この可変長設計の核心は、各バイトの先頭ビットパターンによって「このバイトが何バイト文字の何番目か」を一意に判定できる点にあります。
- ASCII 文字 (英数字・記号): 1 バイト — 先頭ビットが 0 で始まる (0xxxxxxx)。有効ビットは 7 ビットで、128 文字を表現
- ラテン文字の拡張、ギリシャ文字など: 2 バイト — 先頭ビットが 110 で始まる (110xxxxx 10xxxxxx)。有効ビットは 11 ビットで、U+0080〜U+07FF の 1,920 文字を表現
- 日本語 (ひらがな・カタカナ・漢字): 3 バイト — 先頭ビットが 1110 で始まる (1110xxxx 10xxxxxx 10xxxxxx)。有効ビットは 16 ビットで、U+0800〜U+FFFF の約 63,000 文字を表現
- 絵文字、一部の特殊文字: 4 バイト — 先頭ビットが 11110 で始まる (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)。有効ビットは 21 ビットで、U+10000〜U+10FFFF の約 100 万文字を表現
この設計の巧妙な点は「自己同期性」にあります。バイト列の途中から読み始めても、先頭ビットパターンを見るだけで文字の境界を特定できます。継続バイトは必ず 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 バイト |
| 絵文字対応 | 非対応 | 非対応 |
| 多言語対応 | 日本語のみ | 日本語のみ |
| 現在の推奨度 | 非推奨 (レガシー用途のみ) | 非推奨 (レガシー用途のみ) |
失敗パターン: 文字数とバイト数の混同で起きる事故
文字数とバイト数の違いを理解していないと、以下のような深刻なトラブルが発生します。
- データベースでデータが切れる: MySQL の古い utf8 (utf8mb3) エンコーディングでは 1 文字最大 3 バイトまでしか扱えない。絵文字 (4 バイト) を含むデータを INSERT すると、エラーが発生するか、絵文字以降のデータが消失する。utf8mb4 への移行が必須。なお、MySQL 8.0 以降ではデフォルトの文字セットが utf8mb4 に変更されたが、5.7 以前からアップグレードしたシステムでは旧設定が引き継がれるため注意が必要
- CSV ファイルの文字化け: UTF-8 で保存した CSV を Excel で開くと文字化けする。これは Excel が BOM (Byte Order Mark) なしの UTF-8 を Shift_JIS と誤認するため。UTF-8 BOM 付き (先頭に 3 バイトの EF BB BF を付加) で保存すると解決する。ただし、macOS 版の Excel は BOM なし UTF-8 を正しく認識するため、この問題は主に Windows 環境で発生する
- API のペイロードサイズ超過: 「1,000 文字以内」と思って日本語テキストを送信したら、UTF-8 では最大 3,000 バイトになり、API のボディサイズ制限に引っかかる。AWS API Gateway のデフォルトペイロード上限は 10 MB だが、Lambda の同期呼び出しは 6 MB が上限。さらに、Base64 エンコードが介在する場合はデータ量が約 33% 増加するため、実質的な上限はさらに低くなる
- URL エンコーディングの膨張: 日本語を URL に含めると、1 文字が「%E3%81%82」のように 9 文字 (3 バイト × 3) に膨張する。URL の実質的な上限は約 2,000 文字とされるため、日本語を多く含む URL は予想以上に早く上限に達する。具体的には、日本語のみの URL パスは約 220 文字で 2,000 文字の上限に到達する計算になる
- メール送信の文字化け: 日本語メールは歴史的に ISO-2022-JP (JIS コード) で送信される慣習がある。UTF-8 で送信すると、古いメールクライアントで文字化けが発生する場合がある。RFC 6532 で UTF-8 メールが標準化されたが、対応していないメールサーバーも依然として存在する
- GraphQL のクエリサイズ制限: GraphQL サーバーの多くはクエリ文字列のサイズ制限をバイト数で設定している。日本語の変数値を含むクエリは、英語のみの場合と比べて同じ文字数でも約 3 倍のバイト数を消費するため、クエリの複雑さ制限に予想外に早く到達する
実務で注意すべきポイント
文字数とバイト数の違いは、以下のような実務場面で問題を引き起こすことがあります。
- データベースのカラム定義: VARCHAR(255) が「255 文字」なのか「255 バイト」なのかは DBMS によって異なる。MySQL の utf8mb4 では VARCHAR(255) は 255 文字を意味し、最大 1,020 バイトを消費する。一方、Oracle Database のデフォルト設定では VARCHAR2(255) は 255 バイトを意味するため、日本語 85 文字程度で上限に達する。PostgreSQL は常に文字数ベースで、VARCHAR(255) は 255 文字を保証する
- API のリクエストサイズ制限: 多くの API はバイト数で制限を設けている。日本語テキストは英語の約 3 倍のバイト数を消費する。JSON のキー名やメタデータのオーバーヘッドも加算されるため、実際に送信できる本文の文字数はさらに少なくなる
- SMS の文字数制限: 日本語 SMS は 70 文字 (全角) が 1 通の上限。半角英数字のみなら 160 文字まで送信可能。これは SMS が GSM 7-bit エンコーディング (7 ビット × 160 = 1,120 ビット) と UCS-2 エンコーディング (16 ビット × 70 = 1,120 ビット) を使い分けるためで、物理的なデータ量は同じ 140 バイトに収まる設計になっている
- ファイルサイズの見積もり: テキストファイルのサイズは文字数ではなくバイト数で決まる。日本語 1 万文字のテキストファイルは UTF-8 で約 30 KB、改行コードが CRLF (Windows) の場合はさらに改行数分のバイトが加算される
- 文字列の切り詰め処理: バイト数で切り詰めると、マルチバイト文字の途中で切れて文字化けが発生する。UTF-8 の場合、不正な切り詰めは先頭ビットパターンで検出できるため、切り詰め後に継続バイト (10xxxxxx) で終わっていないかを検証するのが安全な実装パターン
| テキスト例 | 文字数 | 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 を返します。「見た目の文字数」と「内部的な文字数」が大きく乖離する典型例であり、文字数カウントの実装で最も注意が必要なケースです。
プロのエンジニアが実践するテクニック
文字数とバイト数の問題に日常的に対処しているプロのエンジニアが実践しているテクニックを紹介します。
- 「バイト数 ÷ 3」の概算ルール: 日本語テキストの文字数を素早く概算するには、UTF-8 のバイト数を 3 で割る。たとえば 3,000 バイトのテキストは約 1,000 文字。英数字が混在する場合は実際にはもう少し多くなる。一般的な日本語の技術文書では英数字が 20〜30% 混在するため、実測のバイト/文字比率は 2.4〜2.7 程度になることが多い
- MySQL は必ず utf8mb4 を使う: utf8 (utf8mb3) は 3 バイトまでしか扱えず、絵文字が保存できない。新規プロジェクトでは必ず utf8mb4 を指定する。既存システムの移行時は
ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4を使用する。移行時の注意点として、utf8mb4 ではインデックスキーの最大長が 3,072 バイト (InnoDB) であるため、VARCHAR(255) のカラムにインデックスを張ると 255 × 4 = 1,020 バイトを消費する。複合インデックスの場合は合計がこの上限を超えないか確認が必要。MySQL データベース設計の書籍で詳しく学べます - 文字列切り詰めは「文字数」で行う: バイト数で切り詰めるとマルチバイト文字の途中で切れる。Python なら
text[:100](文字数)、JavaScript なら[...text].slice(0, 100).join('')(コードポイント単位) を使う。ただし、コードポイント単位の切り詰めでも ZWJ 結合絵文字の途中で切れる可能性がある。完全な書記素クラスタ単位での切り詰めが必要な場合は、JavaScript のIntl.SegmenterAPI を使用する - BOM の有無を意識する: UTF-8 の BOM (EF BB BF) は 3 バイト。ファイルの先頭にこの 3 バイトがあると、JSON パーサーがエラーを返す場合がある。プログラムで読み込むファイルは BOM なし、Excel で開く CSV は BOM 付きが安全。シェルスクリプトの先頭に BOM があると
#!/bin/bashのシバンが認識されず実行に失敗するケースもある - サロゲートペアに注意する: JavaScript の
String.lengthは UTF-16 コードユニット数を返すため、絵文字や一部の漢字で実際の文字数とズレる。正確な文字数が必要な場合は[...str].length(スプレッド構文) を使う。なお、正規表現でもuフラグを付けないとサロゲートペアが 2 文字として扱われ、/^.$/が絵文字にマッチしない問題が発生する - エンコーディング検出の落とし穴: 自動エンコーディング検出は完全ではない。短いテキストほど誤検出率が高く、特に「日本語の Shift_JIS テキスト」と「UTF-8 テキスト」はバイトパターンが重複する領域があるため、数十バイト程度のテキストでは正確な判定が困難。確実な方法は、データの生成元でエンコーディングを明示的に指定し、メタデータとして伝搬させること
実例: 文字コードの選択ミスで起きた大規模障害
文字コードの問題は、時に大規模なシステム障害を引き起こします。以下は実際に起こりうるパターンです。
- SNS の絵文字対応: あるサービスがデータベースに MySQL の utf8 (3 バイト上限) を使用していたところ、ユーザーが絵文字を含む投稿をした際にデータが破損するという問題が発生。utf8mb4 への移行には大規模なデータベースマイグレーションが必要となり、数日間のメンテナンスを要したとされる事例がある。移行時の技術的な課題として、テーブルのロック時間、インデックスの再構築、レプリケーション遅延の管理が挙げられる
- 行政システムの氏名問題: 日本の行政システムでは、JIS 第 1・第 2 水準に含まれない漢字 (いわゆる外字) の扱いが長年の課題となっている。「髙」(はしごだか) や「﨑」(たつさき) などの異体字は、Shift_JIS では表現できず、システムによっては登録できない場合がある。2023 年に策定されたデジタル庁の「文字環境導入実践ガイドブック」では、行政システムにおける文字コードの統一方針として Unicode の採用が推奨されている
- 検索エンジンのインデックス問題: Web ページの Content-Type ヘッダーと実際のエンコーディングが不一致の場合、検索エンジンがページ内容を正しくインデックスできない。たとえば、ヘッダーで UTF-8 を宣言しているのに実際のコンテンツが Shift_JIS で記述されていると、検索結果に文字化けしたスニペットが表示されたり、日本語キーワードでの検索にヒットしなくなる
- 国際化対応でのタイムゾーン名: タイムゾーン名に非 ASCII 文字を含む地域 (例: 日本語環境での「日本標準時」) をログに記録する際、ログ収集システムが ASCII のみを想定していると、タイムスタンプの解析に失敗する。ログのエンコーディングは UTF-8 に統一し、パーサーもマルチバイト対応にしておくことが重要
プログラミング言語ごとの文字数カウント
プログラミング言語によって、文字列の「長さ」が何を返すかは異なります。この違いを把握していないと、同じロジックを別の言語に移植した際にバグが混入します。
| 言語 | 長さ取得メソッド | 返す値 | 「🎉」の長さ | 「𠮷」の長さ | 正確な文字数の取得方法 |
|---|---|---|---|---|---|
| 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) を標準とし、文字列の切り詰めは必ず文字数単位で行い、絵文字や異体字を含むテストケースを用意しておくことが、エンコーディング関連のトラブルを未然に防ぐ最善の方法です。テキストの文字数やバイト数を手軽に確認したい場合は、文字数カウントス をぜひご活用ください。