#06 バイナリデータ入門

文字コード——ASCIIとUTF-8でテキストをバイトで表す

「文字列」はバイト列である

プログラムで "Hello" と書くとき、メモリ上には何が並んでいるのでしょうか?答えはバイトの列です。文字列とは、文字をバイト列にエンコードしたもの です。

このエンコード方法のルールが 文字コード(character encoding) です。


ASCII——最初の文字コード

ASCII(アスキー) は1960年代に作られた、英数字・記号を7ビット(0〜127)で表す規格です。

主要な文字とそのバイト値:

文字10進数16進数2進数
A650x4101000001
B660x4201000010
Z900x5A01011010
a970x6101100001
z1220x7A01111010
0480x3000110000
9570x3900111001
スペース320x2000100000
!330x2100100001

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進数
AU+004165
U+304212354
U+6F2228450
🎉U+1F389127881

Unicodeは「番号の定義」であって、バイト列への変換方法(エンコード)は別に定義されます。そのエンコード方式の中で最も広く使われているのが UTF-8 です。


UTF-8——可変長エンコードの仕組み

UTF-8はコードポイントの値によって使うバイト数が変わります。

コードポイント範囲バイト数バイトパターン
U+0000 〜 U+007F1バイト0xxxxxxx
U+0080 〜 U+07FF2バイト110xxxxx 10xxxxxx
U+0800 〜 U+FFFF3バイト1110xxxx 10xxxxxx 10xxxxxx
U+10000 〜 U+10FFFF4バイト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の補数・文字コードと、コンピュータがデータを扱う根本を一通り押さえました。