一覧PG-09-2 · 文字列の処理

文字コードとエンコード・デコード

日本語を含む同じ文字列を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

1
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
2
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
あ
3
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
4
import codecs; decoder = codecs.getincrementaldecoder("utf-8")(); print(repr(decoder.decode(data[:2], final=False))); print(repr(decoder.decode(data[2:], final=True)))
'A'
'あ'
5
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
6
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
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 False
['U+304B', 'U+3099']
True

演習完了