文字コードと文字化け
文字コードと文字化けとは
コンピュータはバイト(0〜255の数値)しか扱えません。「あ」という文字をそのまま保存も送信もできないので、文字に番号を割り当て、その番号をバイト列に変換する取り決めが必要になります。これが文字コードです。
文字化けは、この取り決めが書いた側と読む側で食い違ったときに起きます。重要なのは、多くの場合バイト列そのものは無事だという点です。正しい取り決めで読み直せば元に戻ります。逆に、化けた状態のまま保存し直すと本当に壊れます。
文字集合と符号化方式は別物
初学者が最初に混乱するのは、「文字コード」という言葉が2つの層をまとめて指しているからです。
| 層 | 役割 | 例 |
|---|---|---|
| 文字集合 | どの文字にどの番号を振るか | Unicode(「あ」は U+3042) |
| 符号化方式 | その番号を何バイトでどう表すか | UTF-8 / UTF-16 / Shift_JIS |
Unicode は世界中の文字に符号位置(コードポイント)を与える巨大な一覧表で、それ自体はバイトの並びを決めません。同じ U+3042 でも、符号化方式が違えばディスクに並ぶバイトは変わります。
「あ」のバイト列
UTF-8 : E3 81 82 (3バイト)
UTF-16BE : 30 42 (2バイト)
Shift_JIS : 82 A0 (2バイト・Unicodeとは別系統の文字集合)
現在の Web では UTF-8 が事実上の唯一の選択肢です。ASCII の範囲(半角英数字)が1バイトのまま変わらないので既存の仕組みを壊さず、バイトの並び順の曖昧さも生じません。日本語は3バイト、絵文字や一部の漢字は4バイトを使います。「1文字=1バイト」ではないことが、後述するデータベースや文字数制限の落とし穴につながります。
charsetの宣言は層ごとに必要
受け取った側は、バイト列を見ただけでは符号化方式を確実には判別できません。だから送る側が明示する必要があり、その宣言はデータが通る層ごとに別々に存在します。
- [[HTML]] —
<meta charset="utf-8">を<head>の先頭近くに置く - [[HTTP]] — レスポンスヘッダーの
Content-Type: text/html; charset=utf-8 - [[JSON]] — 仕様上 UTF-8 が前提。非ASCII文字はそのまま書いても
\u3042形式でエスケープしても同じ意味になる - メール — 本文は
Content-Typeの charset で示し、件名などのヘッダーは ASCII しか運べないため MIME の=?UTF-8?B?...?=形式に符号化する([[email-protocols]])
ここで詰まりやすいのが優先順位です。HTML の meta と HTTP ヘッダーの指定が食い違った場合、HTTP ヘッダー側が優先されます。「meta を utf-8 に直したのに化けたまま」の原因はたいていこれで、直すべきはサーバーの設定のほうです。
データベースの文字セットと照合順序
[[MySQL]] などの RDB では、文字セットに加えて照合順序(collation)を指定します。文字セットが「保存の形式」、照合順序が「比較と並べ替えのルール」です。
| 指定 | 効いてくる場所 |
|---|---|
| 文字セット(utf8mb4 など) | 保存できる文字の範囲、1文字あたりのバイト数 |
| 照合順序(utf8mb4_general_ci など) | WHERE の一致判定、ORDER BY の並び順、UNIQUE制約の重複判定 |
有名な罠が MySQL の utf8 です。これは最大3バイトまでしか扱えない歴史的な仕様で、4バイト必要な絵文字を保存しようとするとエラーか切り捨てになります。utf8mb4 を指定するのが正解で、mb4 は「最大4バイト」を意味します。
照合順序の ci は case insensitive(大文字小文字を区別しない)、ai は accent insensitive(アクセント記号を区別しない)の略です。区別しない設定では Taro と taro が同じ値とみなされるため、「重複していないはずなのに UNIQUE 制約で登録できない」が起きます。
さらに、カラムの文字セットが正しくても接続の文字セットが違えば化けます。DB・接続・アプリの3か所を揃える、と覚えておくと切り分けが速くなります。
文字化けを層ごとに切り分ける
化けた文字の見た目は、どこで何を間違えたかの手がかりになります。
| 見た目 | 典型的な原因 |
|---|---|
| 「縺」「繧」が並ぶ | UTF-8 のバイト列を Shift_JIS として読んだ |
| 「ã」「â」が並ぶ | UTF-8 のバイト列を Latin-1 として読んだ |
| 「?」や「□」ばかりになる | 変換先に無い文字を捨てた(元に戻せない) |
| 先頭に見えない文字や妙な記号が付く | BOM が付いている |
切り分けは川上から順に行います。DBに入っている値 → APIのレスポンス → ブラウザの表示、と1層ずつ確認して最初に化けている層を特定します。表示だけの問題なら宣言を直せば戻りますが、化けた状態で保存し直すと元のバイトが失われて復元できません。原因究明より先に、書き込みを止めるのが鉄則です。
BOM(Byte Order Mark)は、ファイル先頭に置かれる U+FEFF という印です。本来は UTF-16 のバイト順を示すためのもので UTF-8 には不要ですが、Windows 系のツールが「UTF-8 with BOM」として付けることがあります。付いていると JSON.parse が先頭で失敗する、シェルスクリプトの #! が効かない、CSVの1列目の見出し名だけ一致しない、といった不可解な不具合になります。
逆に BOM が役立つ場面もあります。Excel は BOM の無い UTF-8 の CSV を環境既定の文字コードとして開くため日本語が化けますが、BOM が付いていれば UTF-8 と判定します。「BOM を付けるべきか」に絶対的な正解はなく、渡す相手が何で読むかで決める判断項目です。
初学者向けポイント
- 迷ったら UTF-8(ファイルは BOM 無し)で統一する。選択肢を減らすことが最大の事故防止になります
- 化けを見たら、まず「壊れたのか、読み方を間違えているだけか」を判断する。後者なら焦って保存し直さない
- 「文字数」は符号化とは別の話です。バイト数・コードポイント数・見た目の文字数は一致しません(絵文字は見た目1文字でも4バイト、肌の色や国旗の絵文字は複数のコードポイントの組み合わせ)
- [[i18n]] は「文言を翻訳できる形にする」設計、文字コードは「その文字を壊さずに運ぶ」土台です。翻訳の仕組みを入れる前に符号化を揃えておく
- [[tokenization]] のトークン分割とは層が違います。文字コードは「文字→バイト列」、トークナイザは「文字列→モデルが扱う単位」への変換で、目的も粒度も別物です
関連技術とのつながり
- [[HTTP]] — Content-Type ヘッダーの charset がブラウザの解釈を決める(meta より優先)
- [[HTML]] — meta charset による宣言。ページ表示の最終地点
- [[MySQL]] — utf8mb4 と照合順序の選択が、保存できる文字と検索結果を左右する
- [[i18n]] — 多言語対応の前提となる符号化の統一
- [[tokenization]] — 同じ「文字を分ける」でも扱う層が違う処理
Q: Unicode と UTF-8 の関係として正しいのはどれ?
- [x] Unicodeは文字に番号を振る文字集合、UTF-8はその番号をバイト列にする符号化方式
- [ ] Unicodeは日本語用、UTF-8は英語用の文字コード
- [ ] どちらもまったく同じものの別名
解説: 文字集合と符号化方式は別の層です。同じ U+3042 でも符号化方式が違えばバイト列は変わります。
Q: HTMLのmeta charsetとHTTPレスポンスヘッダーのcharsetが食い違うとき、優先されるのはどれ?
- [ ] HTMLのmeta charset
- [x] HTTPレスポンスヘッダーのContent-Type
- [ ] どちらも無視され常にShift_JISとして解釈される
解説: HTTPヘッダー側が優先されます。metaを直しても化けるなら、サーバーの設定を疑います。
Q: MySQLで絵文字が保存できないときの原因として本文が挙げたのはどれ?
- [ ] 照合順序が ci になっているから
- [ ] ファイルにBOMが付いていないから
- [x] 文字セットが最大3バイトまでの utf8 で、4バイト必要な文字が入らないから
解説: utf8mb4 を指定します。mb4 は「最大4バイト」の意味で、絵文字などの4バイト文字を保存できます。