文字コードとエンコード・デコード
日本語を含む同じ文字列をUTF-8へ変換し、添字と長さを比べます。切れたバイト列、異なる文字コード、置換による情報の欠落を確認し、文字の正規化も試します。
説明
strはUnicodeのコードポイントを扱い、bytesは0〜255の数値を並べたものです。文字をファイルや通信へ渡すときは符号化し、受け取ったバイト列は対応する文字コードで復号します。
日本語を含む同じ文字列をUTF-8へ変換し、添字と長さを比べます。切れたバイト列、異なる文字コード、置換による情報の欠落を確認し、文字の正規化も試します。
1同じ内容を文字とバイトで表示
Aに続いて「あ」を置いた文字列をUTF-8へ符号化します。
text = "Aあ"二つの文字; data = text.encode("utf-8")UTF-8へ符号化; print(text, len(text), type(text).__name__)文字列の長さと型; print(data.hex(" "), len(data), type(data).__name__)バイトの値と長さコードポイントは2個、UTF-8は41 e3 81 82の4バイトです。Aは1バイト、「あ」は3バイトで、長さを同じ単位として扱えません。
2添字1で得られる値
同じ添字1を文字列とバイト列に使います。
print(text[1], type(text[1]).__name__コードポイントを取得); print(data[1], hex(data[1]), type(data[1]).__name__単一バイトを整数で取得); print(data[1:4].decode("utf-8")一文字分をまとめて復号)strの添字は「あ」、bytesの添字は先頭バイトe3の整数227を返します。「あ」に相当するバイト範囲1:4をまとめて復号すると、元の文字へ戻ります。
3文字の途中で切ったバイト列
先頭2バイト41 e3だけをUTF-8として復号します。
data[:2].decode("utf-8")途中で切れた文字の復号UnicodeDecodeErrorは位置1のe3から始まる文字の後続バイトが足りないことを示します。ネットワークの読込み単位が、文字の境界と一致するとは限りません。
4分割された入力を段階的に復号する
incremental decoderは未完の文字のバイトを保持し、次の入力と合わせます。
import codecs; decoder = codecs.getincrementaldecoder("utf-8")()未完のバイトを保持する復号器; print(repr(decoder.decode(data[:2], final=False)続きがある入力)); print(repr(decoder.decode(data[2:], final=True)残りと終端の通知))最初にAだけ、残りの入力で「あ」が返ります。final=Trueは入力の終端を知らせ、未完のバイトが残ればエラーにします。分割受信で毎回独立にdecodeする場合とは状態の持ち方が違います。
5別の文字コードでもエラーになるとは限らない
同じUTF-8のバイト列をLatin-1で読み、得たコードポイントを表示します。
wrong = data.decode("latin-1")別の解釈で復号; print([f"U+{ord(c):04X}" for c in wrong])得たコードポイント; print(wrong.encode("latin-1") == data)同じ方式での往復Latin-1は各バイトを直接コードポイントへ対応付けるため、この入力では例外が出ません。しかしU+00E3・U+0081・U+0082は「あ」のU+3042とは異なります。往復できることだけでも、意図する文字コードだとは証明できません。
6置換や無視で失われる情報
UTF-8として不正なffを含むバイト列を、replaceとignoreで処理します。
broken = b"A\xffB"不正なUTF-8を含む試料; replaced = broken.decode("utf-8", errors="replace"置換文字へ変える); ignored = broken.decode("utf-8", errors="ignore"不正部分を落とす); print(repr(replaced), repr(ignored))復号後の差; print(replaced.encode("utf-8").hex(" "))置換後の別のバイト列replaceはU+FFFDの置換文字、ignoreは該当バイトを落とします。再符号化した置換文字はef bf bdで、元のffへは戻りません。エラーを抑える指定は、データを修復して元に戻す機能ではありません。
7見た目が近くてもコードポイント列が違う
「が」を一つのコードポイントと、か+結合濁点の二つで作ります。
import unicodedata; composed = "が"; decomposed = "か\u3099"合成済みと結合文字列; print(len(composed), len(decomposed), composed == decomposed)長さと等価比較; print([f"U+{ord(c):04X}" for c in decomposed])結合前のコードポイント; print(unicodedata.normalize("NFC", decomposed) == composed正規化後の比較)長さ1と2で、そのままでは一致しません。NFCで正規化するとこの例は一致します。len(str)は画面上の文字の見た目の個数を常に数えるものではなく、正規化の採用も用途に応じて決めます。
まとめ
文字の位置とバイトの位置を分け、文字コードと境界を確認します。
例外が出ない復号や置換処理でも、元の情報が保たれるとは限りません。
操作履歴と解説を振り返れます。
Python 3
text = "Aあ"; data = text.encode("utf-8"); print(text, len(text), type(text).__name__); print(data.hex(" "), len(data), type(data).__name__)Aあ 2 str 41 e3 81 82 4 bytes
print(text[1], type(text[1]).__name__); print(data[1], hex(data[1]), type(data[1]).__name__); print(data[1:4].decode("utf-8"))あ str 227 0xe3 int あ
data[:2].decode("utf-8")Traceback (most recent call last): File "<console>", line 1, in <module> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe3 in position 1: unexpected end of data
import codecs; decoder = codecs.getincrementaldecoder("utf-8")(); print(repr(decoder.decode(data[:2], final=False))); print(repr(decoder.decode(data[2:], final=True)))'A' 'あ'
wrong = data.decode("latin-1"); print([f"U+{ord(c):04X}" for c in wrong]); print(wrong.encode("latin-1") == data)['U+0041', 'U+00E3', 'U+0081', 'U+0082'] True
broken = b"A\xffB"; replaced = broken.decode("utf-8", errors="replace"); ignored = broken.decode("utf-8", errors="ignore"); print(repr(replaced), repr(ignored)); print(replaced.encode("utf-8").hex(" "))'A�B' 'AB' 41 ef bf bd 42
import unicodedata; composed = "が"; decomposed = "か\u3099"; print(len(composed), len(decomposed), composed == decomposed); print([f"U+{ord(c):04X}" for c in decomposed]); print(unicodedata.normalize("NFC", decomposed) == composed)1 2 False ['U+304B', 'U+3099'] True
演習完了