文字エンコーディング入門:ASCIIからUTF-8まで
テキストが時々疑問符や変な記号に化ける理由。文字エンコーディングの実践ガイド。
ファイルを開いたら u の代わりに ü が表示される。データベースが名前のはずの場所に ???? を返す。メールの件名に =?UTF-8?B? が散りばめられている。
文字エンコーディング問題の素晴らしい世界へようこそ。
短い歴史
コンピュータは数値を格納します。文字ではありません。だから誰かがどの数値がどの文字を意味するか決める必要がありました。1960年代、ASCIIが0-127の番号を英語の文字、数字、基本記号に割り当てました。「A」は65。スペースは32。シンプルです。
しかしASCIIはたった128文字しかカバーしません。英語には十分。ドイツ語のウムラウト、日本語の漢字、アラビア文字、あるいは人間が実際に使う何千もの他の文字には対応できません。
Unicode以前の混沌
各地域が独自のエンコーディングを発明しました。西ヨーロッパはISO-8859-1。日本はShift-JIS。ロシアはKOI8-R。中国はGB2312。それぞれ自分のエコシステム内では問題なく動きました。混ぜた瞬間、すべてが壊れました。
あるエンコーディングで保存したファイルを別のエンコーディングで開くと文字化けが起きます——見たことがあるはずの、間違った文字がめちゃくちゃに表示されるあの現象です。UTF-8ファイルをISO-8859-1として読むと cafe が café になります。
Unicodeがマッピング問題を解決した
Unicodeはすべての文字にユニークな番号(「コードポイント」と呼ばれる)を与えました。ラテンのAはU+0041。雪だるまはU+2603。すべての絵文字、すべての文字体系、すべての数学記号が固有のコードポイントを持ちます。15万文字以上、そして増え続けています。
ただしUnicodeはマッピングに過ぎません。その数値をバイトとしてどう格納するかは定めていません。それがエンコーディングの役割です。
UTF-8:勝者となったエンコーディング
UTF-8はインターネットの大部分がUnicodeテキストを格納する方法です。重要なトリック:文字ごとに可変長のバイト数を使います。
- ASCII文字(英字、数字):各1バイト
- ヨーロッパのアクセント付き文字:各2バイト
- アジアの文字(CJK):各3バイト
- 絵文字や珍しい記号:各4バイト
つまりUTF-8の英語テキストはASCIIと同一です。古いシステムが壊れません。それでいて世界中のあらゆる文字を表現できます。
現在、ウェブサイトの98%以上がUTF-8を使用しています。エンコーディング戦争は終わり、UTF-8が勝ちました。
UTF-8 vs UTF-16 vs UTF-32
UTF-8: 可変幅(1-4バイト)。英語中心のテキストに効率的。ウェブ標準。
UTF-16: 可変幅(2または4バイト)。JavaScript、Java、Windowsが内部的に使用。すべての文字が最低2バイトなので、ASCIIテキストには非効率的。
UTF-32: 固定幅(文字あたり4バイト)。シンプルだが無駄が多い。格納や送信にはほとんど使われない。
JavaScriptの string.length は文字ではなくUTF-16コードユニットを数えます。だから "😀".length は1ではなく2を返します。
エンコーディングが壊れるとき
間違ったエンコーディングでファイルを読む。 バイト自体は正常ですが、解釈が間違っています。解決策:開くときに正しいエンコーディングを指定する。
データベースの文字セット不一致。 アプリがUTF-8を送信しているのに、データベースのカラムがlatin1に設定されている。ASCII外の文字が壊れます。解決策:データベースを utf8mb4 に設定する(MySQLの utf8 だけでは3バイト文字しか処理できません)。
HTTPのcharsetヘッダー欠落。 サーバーが Content-Type: text/html; charset=utf-8 を送信しないと、ブラウザが推測しなければなりません。時々間違えます。
実践的なヒント
常にUTF-8を使う。 使わない非常に具体的な理由がない限り、UTF-8がすべてに適した選択です。
エンコーディングを明示的に宣言する。 HTMLでは:<meta charset="utf-8">。HTTPでは:Content-Type: text/html; charset=utf-8。システムに推測させないでください。
文字列の長さに注意する。 JavaScriptでの文字カウントは、絵文字や結合文字で複雑になります。正確なカウントには Array.from(str).length や Intl.Segmenter APIを使いましょう。
BOMに気をつける。 バイト順マーク(U+FEFF)がUTF-8ファイルの先頭に現れることがあります。目に見えませんがパーサーを壊す可能性があります。一部のエディタが静かに追加します。
文字エンコーディングは刺激的なトピックではありませんが、理解すればデバッグの時間を何時間も節約できます。あらゆる場所でUTF-8を使い、明示的に宣言し、文字化けを見たら正確にどこを見ればいいか分かるようになります。