文字コードとバイト数の基礎
文字が数値として保存される仕組みと、文字数・データ量の関係を学びます。
説明
コンピューターが扱うデータは、0と1の組合せで表されます。この0または1の一桁がビットで、8ビットをまとめたものが1バイトです。文字も、決められた規則に従ってバイト列に変換してから保存します。この規則を文字コードと呼びます。
このモジュールでは、「文字がバイト列になるとどう見えるのか」「同じ文字数でもデータ量が変わるのはなぜか」を学びます。まず1バイトの値を2進数と16進数で表示し、次に英字と日本語をASCII・UTF-8という文字コードで変換します。最後に、そのバイト数を使って容量の単位と転送時間の計算を説明します。
11バイトの16進表記
8桁の2進数をそのまま書くと長いため、バイトは16進数で表すことがよくあります。16進数は0〜9とA〜Fを使い、Aは10、Fは15を表します。1桁が4ビットに相当するので、1バイトは16進数2桁で表せます。
例として、16進数の「A5」をバイト列に変換し、その内容を表示します。
raw =変数rawに保存 bytes.fromhex('a5')16進表記からバイト列を作る; raw.hex(' ')16進数で表示'a5'と表示されました。大文字のA5と小文字のa5は同じ値です。rawに入っているのは、文字のAと5を保存した2バイトではなく、16進数A5で表す1バイトです。
2バイト数と、その中に入っている値
同じ1バイトについて、長さ・値・2進表記を並べます。
len(raw)バイト数, raw[0]先頭バイトの値, format(raw[0], '08b')8桁の2進数で表示結果の1はバイト数、165は10進数の値、10100101は同じ値の2進表記です。2進数の各桁は、右から1、2、4、8、16、32、64、128を表します。1になっている桁を足すと128+32+4+1=165です。
31バイトで表せる値の範囲
00とFFでもバイト数を調べます。値の大きさによって、必要な長さが変わるかを確認してください。
[(x.hex()16進表記, len(x)バイト数, x[0]先頭バイトの値) for x in (bytes.fromhex('00'), bytes.fromhex('ff'))00とFFを順に処理]00の値は0、FFの値は255ですが、どちらも1バイトです。8ビットの組合せは2⁸=256通りあります。負の数を扱わない符号なし整数なら、1バイトで0〜255を表せます。
4ASCIIでの英字の表し方
ASCIIでは、英字や数字などに0〜127の番号が割り当てられています。例として文字「A」をASCIIへ変換します。
text = 'A'文字列を保存; raw = text.encode('ascii')ASCIIへ符号化; raw.hex(' ')16進表記, raw[0]10進数の値文字「A」に対応する値は、16進数で41、10進数で65です。最初に使った16進数A5とは異なります。文字と番号の対応は、説明欄の末尾にある「対応表・資料」で全128値を確認できます。
5ASCIIとUTF-8に共通する文字
UTF-8は、日本語を含む多くの文字を表せる文字コードです。まず「ABC」をUTF-8へ変換します。Unicodeで文字に割り当てられた番号を、コードポイントと呼びます。
text = 'ABC'3文字の文字列; raw = text.encode('utf-8')UTF-8へ符号化; [ord(c) for c in text]各文字のコードポイント, raw.hex(' ')バイト列の16進表記「A」「B」「C」のコードポイントは10進数で65・66・67、保存するバイトは16進数で41・42・43です。ASCIIに含まれる文字は、UTF-8でも同じ1バイトで表されます。
6UTF-8では「あ」が3バイトになる
次は「Aあ」です。各文字のコードポイントと、UTF-8で保存するバイト列を並べます。
text = 'A\u3042'「Aあ」を保存; raw = text.encode('utf-8')UTF-8へ符号化; [f'U+{ord(c):04X}' for c in text]各文字のコードポイントを16進表示, raw.hex(' ')バイト列の16進表記「A」はU+0041、「あ」はU+3042です。一方、UTF-8で保存するバイト列は41 E3 81 82で、計4バイトになります。「あ」のコードポイント3042を、そのまま2バイトにして保存するわけではありません。
7文字数とバイト数が異なる理由
「Aあ」の文字数と、UTF-8に変換したバイト列の長さを調べます。
len(text)コードポイント数, len(raw)バイト数, len(raw) * 8ビット数「Aあ」は2コードポイントですが、UTF-8では4バイト、つまり32ビットです。「A」は1バイト、「あ」は3バイトなので、文字数だけではデータ量を求められません。
なお、結合文字などを含む文字列では、見た目の文字数とコードポイント数も一致しないことがあります。
8UTF-8のビットの並び
UTF-8は、先頭の数ビットで、そのバイトが文字の始まりなのか、続きなのかを表します。各バイトを2進数にして、その並びを確認します。
[format(b, '08b')各バイトを8桁の2進数にする for b in rawrawの全バイトを順に処理]「A」のバイトは0で始まります。これはASCII範囲の文字を1バイトで表す形式です。「あ」は1110で始まるバイトと、10で始まる二つのバイトになっています。
1110と10を除いた4+6+6ビットをつなげると0011 0000 0100 0010です。16進数にすると3042となり、「あ」のコードポイントが得られます。
9バイト列から文字列に戻す処理
文字列をバイト列に変換する処理を符号化、その逆を復号と呼びます。UTF-8で復号すると元の文字列に戻るかを確認します。
raw.decode('utf-8')UTF-8で文字列に戻す, raw.decode('utf-8') == text元の文字列との一致を判定「Aあ」に戻り、比較結果はTrue、つまり等しいという結果になりました。保存されたバイト列を文字として扱うには、符号化したときと同じ文字コードで復号します。
10kBとKiBの換算
kBは1000バイト、KiBは1024バイトを1単位とする表記です。同じバイト列を1000個つなげ、二つの単位で容量を求めます。
payload = raw * 1000バイト列を1000個つなぐ; len(payload)B単位, len(payload) / 1000kB単位, len(payload) / 1024KiB単位4000 Bを1000で割ると4 kB、1024で割ると3.90625 KiBです。どちらも同じ4000バイトを表しています。単位が違うと数値も変わるため、容量を確認するときは単位まで読む必要があります。
11GBとGiBで容量表示が変わる理由
GBは10⁹バイト、GiBは2³⁰バイトを1単位とします。1 GBに相当するバイト数を、それぞれの単位で表示します。
size = 10**910⁹バイト; print(f'{size / 10**9:.3f} GB / {size / 2**30:.3f} GiB')GBとGiBへ換算し小数点以下3桁で表示1.000 GBが、GiBでは約0.931と表示されました。数値の違いは単位の換算によるものです。保存されているデータ量が減ったわけではありません。
12通信速度から転送時間を求める式
通信速度のMbit/sは、1秒当たりに転送するメガビット数です。ファイル容量のMBはメガバイトなので、そのまま割り算はできません。100 MBのデータを80 Mbit/sで送る例で、時間を計算します。
size = 100 * 10**6100 MBのバイト数; bits_per_second = 80 * 10**680 Mbit/s; size / (bits_per_second / 8)速度をB/sに直して転送秒数を計算80 Mbit/sを8で割ると10 MB/sです。100 MB÷10 MB/sで、転送時間は10秒になります。この計算はデータ部分を一定の速度で送れる場合の値で、通信ヘッダーや再送による追加時間は含みません。
まとめ
文字「Aあ」は2コードポイントですが、UTF-8では41 E3 81 82の4バイトになります。文字に割り当てた番号と、保存するバイト列は、文字コードの規則に従って対応しています。
容量や転送時間の計算では、bitとB、kBとKiBなどの単位を確認します。同じ量でも、使う単位によって表示される数値は変わります。
操作履歴と解説を振り返れます。
Python 3
raw = bytes.fromhex('a5'); raw.hex(' ')'a5'
len(raw), raw[0], format(raw[0], '08b')(1, 165, '10100101')
| 重み | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
|---|---|---|---|---|---|---|---|---|
| ビット | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 |
[(x.hex(), len(x), x[0]) for x in (bytes.fromhex('00'), bytes.fromhex('ff'))][('00', 1, 0), ('ff', 1, 255)]text = 'A'; raw = text.encode('ascii'); raw.hex(' '), raw[0]('41', 65)text = 'ABC'; raw = text.encode('utf-8'); [ord(c) for c in text], raw.hex(' ')([65, 66, 67], '41 42 43')
text = 'A\u3042'; raw = text.encode('utf-8'); [f'U+{ord(c):04X}' for c in text], raw.hex(' ')(['U+0041', 'U+3042'], '41 e3 81 82')
| 文字 | コードポイント | UTF-8 | バイト数 |
|---|---|---|---|
| A | U+0041 | 41 | 1 |
| あ | U+3042 | E3 81 82 | 3 |
len(text), len(raw), len(raw) * 8(2, 4, 32)
[format(b, '08b') for b in raw]['01000001', '11100011', '10000001', '10000010']
| 位置 | バイト | 形式を示すビット | コードポイントのビット |
|---|---|---|---|
| 1 | E3 | 1110 | 0011 |
| 2 | 81 | 10 | 000001 |
| 3 | 82 | 10 | 000010 |
raw.decode('utf-8'), raw.decode('utf-8') == text('Aあ', True)payload = raw * 1000; len(payload), len(payload) / 1000, len(payload) / 1024(4000, 4.0, 3.90625)
size = 10**9; print(f'{size / 10**9:.3f} GB / {size / 2**30:.3f} GiB')1.000 GB / 0.931 GiB
size = 100 * 10**6; bits_per_second = 80 * 10**6; size / (bits_per_second / 8)10.0
演習完了