基礎●●○○○

文字コード

もじコード

文字と数値の対応規則。ASCIIからUnicodeへ至る歴史が文字化けの正体を説明する。

概要

文字コードは、文字と数値の対応規則です。コンピュータが直接扱えるのは2進数のビット列だけなので、「A は 65、あ は 12354」のように文字に番号を割り振り、その番号をバイト列として記録する — この約束事全体を文字コード(文字エンコーディング)と呼びます。

普段は言語やフレームワークが自動で処理してくれるため意識せずに済みますが、ひとたび約束がすれ違うと「譁�蟄怜喧縺�」のような文字化けとなって表面化します。CSVを開いたら日本語が崩れた、メールの件名が読めない、絵文字を保存したらデータベースがエラーを吐いた — これらはすべて文字コードの語彙で説明できる現象です。テキストを扱わないシステムはほぼ存在しない以上、すべてのエンジニアにとっての基礎教養といえます。

なぜ生まれたか

出発点は1963年の ASCII です。英数字と記号あわせて128文字に0〜127の番号を割り振った7ビットの規格で、英語圏ではこれで十分でした。しかし世界には英語以外の文字があります。各国は「ASCIIの空き領域や拡張領域に自国の文字を詰め込む」独自規格を作り始め、日本だけでも Shift_JIS、EUC-JP、ISO-2022-JP が併存、欧州には Latin-1 系の亜種が乱立しました。同じバイト列でも規格が違えば別の文字に読める — 文字化けはこの乱立の必然的な帰結です。さらに各規格は互いに変換しづらく、多言語が混ざった文書(日本語と韓国語を1ファイルに)はそもそも表現できませんでした。

この分断を解決するために生まれたのが Unicode です。「世界中のすべての文字に、ただ一つの番号体系で番号を振る」という統一プロジェクトで、1991年の初版から拡張を続け、現在では15万近い文字(歴史的文字や絵文字を含む)を収録しています。Webの普及で国境を越えたテキスト交換が爆発的に増えたことが追い風となり、現在ではWebページの9割以上が Unicode(UTF-8)で書かれています。文字コードの歴史は、ローカルな最適化が生んだ分断を、グローバルな標準で統一し直した系譜そのものです。

詳細

最重要の区別 — コードポイントと符号化方式は別物

Unicode を理解する鍵は、「文字に番号を振る」段階と「番号をバイト列にする」段階が分かれていることです。前者がコードポイントで、「あ」には U+3042 という番号が与えられています(U+ に続く16進数が番号)。後者が符号化方式で、同じ番号をバイト列に直す方法として UTF-8・UTF-16・UTF-32 の3種類があります。UTF-32 は全文字を固定4バイトで、UTF-16 は2バイトまたは4バイトで、UTF-8 は1〜4バイトの可変長で表します。「Unicode で保存する」という言い方が曖昧なのはこのためで、実際に保存されるバイト列は符号化方式を決めて初めて定まります。

文字 「あ」Unicodeの表コードポイント U+30422進数で00110000010000101110 001110 00000110 000010= E3= 81= 82UTF-8のバイト列 E3 81 82 — 「あ」は3バイト金色はUTF-8が決めるパターンビット。残りの空きにコードポイントのビットを順に詰める
「あ」がバイト列になるまで — コードポイントを決めてから、UTF-8の規則でバイトに詰める

この変換は、好きな文字で自分の手で確かめられます。文字を入力して、コードポイントとUTF-8バイト列への詰め替えを観察してみてください。

⚡ 体験: 文字をUTF-8バイト列に分解する
コードポイント 5見た目 5 文字合計 10 バイト
A」のビット詰め替え — コードポイントの2進表現をUTF-8のパターン(金色)の空きに詰める
U+0041 の2進表現1000001
1バイトのパターンに詰める
UTF-8 バイト列01000001= 41

文字を入力するか、カードをクリックしてみてください。各文字がコードポイントを経てUTF-8のバイト列になる様子が見えます。

UTF-8 が勝った理由

3方式のうち事実上の標準となったのは UTF-8 です。決め手は ASCII との完全互換にあります。UTF-8 では ASCII の128文字をそのまま1バイトで表すため、既存の英語のテキスト・設定ファイル・プロトコルは一切変換せずに「正しい UTF-8」として通用します。番号の大きい文字ほどバイト数が増える可変長設計(欧文1バイト、多くの漢字・かな3バイト、絵文字4バイト)で、先頭バイトのパターンを見ればそこが文字の切れ目だと分かる自己同期性も備えています。HTMLJSON も、現代のWeb標準はデフォルトの符号化として UTF-8 を前提としています。なお URL に日本語を含めると %E3%81%82 のような表記になりますが、これは「あ」の UTF-8 バイト列をパーセント記号付き16進数で書き写したもの — 上の図のバイト列そのものです。

文字化けの正体

文字化けの正体は、データの破損ではなく解釈の食い違いです。バイト列そのものは無事でも、「Shift_JIS で書かれたバイト列を UTF-8 として読む」「UTF-8 を Latin-1 として読む」と、対応表が違うため別の文字列が復元されてしまいます。冒頭の「譁�蟄怜喧縺�」は、UTF-8 の日本語を Shift_JIS として読んだときの典型的な化け方です。

だからこそ、テキストには「これは何の符号化か」の申告が付きます。HTTP レスポンスの Content-Type ヘッダの charset、HTML の meta charset 宣言、データベースの接続・テーブルの文字コード設定がそれです。文字化けの調査とは、この申告と実際のバイト列の食い違いを、データが通る経路(ファイル → アプリ → DB → ブラウザ)のどこで起きたか特定する作業にほかなりません。

サロゲートペア・絵文字・結合文字 — 「1文字」の落とし穴

Unicode の当初の想定は「65536個(16ビット)あれば全文字が収まる」でしたが、収まりませんでした。UTF-16 はこの想定の上に作られていたため、あふれた文字(多くの絵文字や一部の漢字)は16ビット値2個の組「サロゲートペア」で表す拡張が施されました。JavaScript や Java の文字列がこの UTF-16 ベースであるため、"𩸽".length"😀".length が 2 を返す、部分文字列の切り出しで文字が真っ二つになる、といった罠が今も現役です。同根の問題として、MySQL の古い utf8(3バイトまでしか格納できない utf8mb3)に絵文字を保存できずエラーになる事故も有名で、RDBMS では utf8mb4 を指定するのが現在の常識です。

さらに、「人間が1文字と感じる単位」はコードポイント1個とも限りません。国旗の絵文字は地域コード2個の組、家族の絵文字は複数の絵文字を接合子でつないだ列、「が」は「か」+濁点の2コードポイントでも表せます(見た目が同じ文字列がバイト列としては別物になり得るため、比較の前に正規化と呼ばれる揃え直しが必要になることがあります)。文字数のカウント・切り詰め・検索を素朴に実装すると壊れるのはこのためで、「バイト・コードポイント・見た目の1文字は、それぞれ別の単位である」という理解が、文字コードというテーマの最終的な到達点です。