LoginSignup
1
2

More than 3 years have passed since last update.

Pythonと壊れたテキスト(UnicodeDecodeError)

Last updated at Posted at 2019-10-14

何が起きたか

普段は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できるのは驚きでした。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2