何が起きたか
普段はPythonではなくRubyを好んで使う私がしぶしぶPythonでテキストをいじっていると、ある日こんなエラーメッセージに遭遇しました。
Traceback (most recent call last):
File "broken_test.py", line 3, in <module>
line = fo.readline()
File "/opt/pyenv/versions/3.5.7/lib/python3.5/codecs.py", line 321, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x85 in position 17: invalid start byte
アイエエエ! パイソン=サン……can't decodeアイエエエエ!?
どうも調べてみると処理していたテキストファイルのデータの一部が化けているようでした。(実際にはもう少し別の要因により、まっとうな状態でファイルが読めなくなっていたのですが)。ともあれ、ここで処理をあきらめるわけにはいかないので解決策を探しました。
結論
もし UnicodeDecodeError でネットをさまよってここに来た人がいたら、だらだらと話に付き合わせるわけにはいかないので結論を先に書きます。
バイナリとしてファイルを開いてdecodeしてください。以上。
詳細
どこで落ちるか
調べると、壊れたデータに差し掛かったときにプログラムが落ちるのではなく、壊れたファイルを読んでfile objectにアクセスした瞬間に落ちていました。
with open(fname, 'r') as fo:
for line in fo: #ここで落ちる
print(line)
テキストはこんな状態とします。0x11の部分がasciiの範囲外になってます。
000000 4b 75 75 0a 43 72 6f 73 73 69 6e 67 0a 42 72 6f
K u u nl C r o s s i n g nl B r o
000010 6b 85 6e 20 53 65 61 73 0a 57 68 69 72 6c 70 6f
k enq n sp S e a s nl W h i r l p o
000020 6f 6c 0a
o l nl
ともあれ、壊れた部分だけ飛ばすというワークアラウンドができそうにないですね。
readlineではだめでした。
同じく1回目のreadlineですでに落ちてしまいます。
with open(fname, 'r') as fo:
while True:
line = fo.readline() #ここに来た瞬間に落ちる
if line == "":
break
print(line)
バイナリで扱えないか?
エラーメッセージを見る限りutf-8として処理できないと言っているのですから、まずバイナリとしてデータを取ってから検査し、そのうえでテキストデータとして処理すればよさそうです。
問題はバイナリとして取得したデータをテキストのように1行ずつ処理できるかですが……
with open(fname, 'rb') as fo:
while True:
bline = fo.readline()
...
動きました。
上のコードでbline
としているものはバイトデータなので通常のutf8文字列にするときはdecode()
を使います。
with open(fname, 'rb') as fo:
while True:
bline = fo.readline()
line = bline.decode('utf-8', 'replace')
if line == "":
break
print(line)
Kuu
Crossing
Brok�n Seas
Whirlpool
decode()
とは
ここを見るとよいです:
https://docs.python.org/ja/3/library/stdtypes.html?#bytes.decode
decodeの第二引数の既定値はstrictで、このモードだとdecodeに失敗すると死にます。
例えば上のコードでreplaceの部分をstrictにすると下記のようになります。
Kuu
Crossing
Traceback (most recent call last):
File "broken_test.py", line 7, in <module>
line = bline.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x85 in position 4: invalid start byte
replaceにすると、strictではdecodeできない部分を適当に置き換えて出力するようですね。
実際、replaceの文字列を見てみると下記のような感じです。
B r o k � n S e a s
42726f6befbfbd6e20536561730a
bline2 = line.encode('utf-8').hex()
print(bline2)
efbfbd
という値になっており、なんじゃこりゃと検索するとREPLACEMENT CHARACTER
という言葉が出てきます。また一つ勉強になりました。
おわりに
文脈とあんまり関係ないですが、pythonはreferenceを読んだ方が圧倒的にはかどりますね。python reference <検索したい関数名など>
で検索した方が良いです。
あと、rubyだと今回問題になったようなファイルはこともなく処理してくれます。日本と関わりが強いからdecodeの失敗に関する経験値が高いんでしょうね……。
でもバイナリで開いたファイルをreadlineできるのは驚きでした。