文字コード——ASCIIとUTF-8でテキストをバイトで表す
「文字列」はバイト列である
プログラムで "Hello" と書くとき、メモリ上には何が並んでいるのでしょうか?答えはバイトの列です。文字列とは、文字をバイト列にエンコードしたもの です。
このエンコード方法のルールが 文字コード(character encoding) です。
ASCII——最初の文字コード
ASCII(アスキー) は1960年代に作られた、英数字・記号を7ビット(0〜127)で表す規格です。
主要な文字とそのバイト値:
| 文字 | 10進数 | 16進数 | 2進数 |
|---|---|---|---|
A | 65 | 0x41 | 01000001 |
B | 66 | 0x42 | 01000010 |
Z | 90 | 0x5A | 01011010 |
a | 97 | 0x61 | 01100001 |
z | 122 | 0x7A | 01111010 |
0 | 48 | 0x30 | 00110000 |
9 | 57 | 0x39 | 00111001 |
| スペース | 32 | 0x20 | 00100000 |
! | 33 | 0x21 | 00100001 |
A(65)と a(97)の差が 32 = 0x20 であることに気づくと、大文字・小文字変換が「ビットを1つ変えるだけ」で実現できることがわかります。
A = 01000001
a = 01100001 ← 6ビット目だけ違う
“Hi!” はバイト列でどう表されるか
H → 72 (0x48)
i → 105 (0x69)
! → 33 (0x21)
メモリ: [0x48] [0x69] [0x21]
= 72 105 33
テキストエディタでファイルを保存したとき、これが実際にディスクに書き込まれるバイト列です。
ASCIIの限界
ASCIIは7ビット = 128文字しか表せません。日本語・中国語・アラビア語・絵文字……世界の文字はとても128には収まりません。
そこで Unicode が生まれました。
Unicode——世界の文字に番号を振る
Unicodeは世界中のすべての文字に コードポイント(U+xxxx) という番号を割り当てた規格です。
| 文字 | コードポイント | 10進数 |
|---|---|---|
A | U+0041 | 65 |
あ | U+3042 | 12354 |
漢 | U+6F22 | 28450 |
🎉 | U+1F389 | 127881 |
Unicodeは「番号の定義」であって、バイト列への変換方法(エンコード)は別に定義されます。そのエンコード方式の中で最も広く使われているのが UTF-8 です。
UTF-8——可変長エンコードの仕組み
UTF-8はコードポイントの値によって使うバイト数が変わります。
| コードポイント範囲 | バイト数 | バイトパターン |
|---|---|---|
| U+0000 〜 U+007F | 1バイト | 0xxxxxxx |
| U+0080 〜 U+07FF | 2バイト | 110xxxxx 10xxxxxx |
| U+0800 〜 U+FFFF | 3バイト | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 〜 U+10FFFF | 4バイト | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
x の部分にコードポイントのビットを埋め込みます。
ASCII文字(1バイト)の例:A
A = U+0041 = 65 = 01000001
1バイトパターン: 0xxxxxxx
0 1000001
→ 0x41(= 65) ← ASCIIと同じ!
UTF-8はASCIIと完全に互換です。 ASCII文字はUTF-8でもそのまま1バイトで表されます。
日本語(3バイト)の例:あ
あ = U+3042 = 12354
2進数: 0011 0000 0100 0010
3バイトパターン: 1110xxxx 10xxxxxx 10xxxxxx
←4ビット→ ←6ビット→ ←6ビット→
コードポイントのビットを分割:
0011 | 000001 | 000010
テンプレートに埋める:
1110 0011 | 10 000001 | 10 000010
= 0xE3 0x81 0x82
あ はメモリ上で [0xE3] [0x81] [0x82] の3バイトとして保存されます。
絵文字(4バイト)の例:🎉
🎉 = U+1F389 = 127881
2進数: 0001 1111 0011 1000 1001
4バイトパターン: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
ビットを分割:
000 | 011111 | 001110 | 001001
テンプレートに埋める:
11110 000 | 10 011111 | 10 001110 | 10 001001
= 0xF0 0x9F 0x8E 0x89
🎉 は4バイトです。
“Hello, 世界” のバイト列
"Hello, 世界" という文字列がUTF-8でエンコードされると:
H → 0x48
e → 0x65
l → 0x6C
l → 0x6C
o → 0x6F
, → 0x2C
→ 0x20 (スペース)
世 → 0xE4 0xB8 0x96 (3バイト)
界 → 0xE7 0x95 0x8C (3バイト)
合計: 7 + 3 + 3 = 13バイト
文字数は9文字でも、バイト数は13バイトです。
プログラマーが知っておくべきこと
文字数 ≠ バイト数
s = "Hello, 世界"
len(s) # → 9(文字数)
len(s.encode()) # → 13(UTF-8バイト数)
これを混同するとバグの原因になります(文字列の切り取りでマルチバイト文字を壊す、など)。
先頭バイトを見れば何バイト文字かわかる
0xxxxxxx → 1バイト文字(ASCII)
110xxxxx → 2バイト文字の先頭
1110xxxx → 3バイト文字の先頭
11110xxx → 4バイト文字の先頭
10xxxxxx → 2〜4バイト文字の2バイト目以降
バイナリエディタやログでUTF-8のバイト列を見たとき、0xE3 0x81... なら「日本語3バイト文字の先頭」と即座に判断できます。
まとめ
- ASCII:英数字・記号を1バイト(0〜127)で表す。今も現役
- Unicode:世界のすべての文字にコードポイント番号を割り当てた規格
- UTF-8:Unicodeのエンコード方式。ASCII互換、可変長(1〜4バイト)
- 英字は1バイト、日本語・中国語は3バイト、絵文字は4バイト
- 文字数とバイト数は違う——これを忘れるとバグになる
これでバイナリデータ入門シリーズは完了です。ビット・バイト・2進数・16進数・オーバーフロー・2の補数・文字コードと、コンピュータがデータを扱う根本を一通り押さえました。