Python
python3

python3で複数文字コードが含まれるファイルをUnicodeDecodeErrorが出る行を飛ばして処理する

先日、python3でファイルを読み込んで各行を処理するプログラムを書いてる時に UnicodeDecodeError と出会いました。ある行にutf-8で(decodeでき)ない行が紛れ込んでいることが原因でした。

処理としては UnicdeDecodeError が出る行をスキップして処理することにしたので、どのようにしたのか書いておきます。

コードの中にencodeとかdecodeとか出てきますがpython3です。

UnicodeDecodeErrorが途中の行で発生するファイルを作成する

create_file.py
def main():
    with open("hoge.txt", 'wb') as f:
        f.write("あ\n".encode('utf-8'))
        f.write("い\n".encode('utf-8'))
        f.write("う\n".encode('utf-8'))
        f.write("シフトジス\n".encode('shift-jis'))  # この行だけ別の文字コードでencodeする
        f.write("え\n".encode('utf-8'))
        f.write("お\n".encode('utf-8'))
        f.write("か\n".encode('utf-8'))

if __name__ == "__main__":
    main()
$ python create_file.py

上で作成したファイルの中身を普通に吐き出そうとしてみる

output_line.py
def main():
    with open("hoge.txt", 'r') as f:
        line = f.readline()
        while line:
            print(line.rstrip())
            line = f.readline()

if __name__ == "__main__":
    main()
$ python output_line.py
Traceback (most recent call last):
  File "output_line.py", line 9, in <module>
    main()
  File "output_line.py", line 3, in main
    line = f.readline()
  File "/path/to/.pyenv/versions/3.5.1/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 0x83 in position 12: invalid start byte

途中にshit-jisのバイト列が入ってるので怒られます。このエラー文を見ると、 f.readline() の中でutf-8型でdecodeしようとしていることがわかります。その為、ファイルの途中で変なバイト列があると、f.readline()できなくなってしまうようです。try・exceptでエラーが出ないようにしても、結局f.readline()できないので次の行に進めなくなってしまいます。

f.readline()でUnicodeDecodeErrorを出さないようにする方法

簡単で、バイト列として行を受け取るようにします。

先ほどのコードでopenする時に r ではなく rb にすると出力でエラーが出なくなります。

output_line.py変更分
with open("hoge.txt", 'rb') as f:
$ python output_line.py
b'\xe3\x81\x82'
b'\xe3\x81\x84'
b'\xe3\x81\x86'
b'\x83V\x83t\x83g\x83W\x83X'
b'\xe3\x81\x88'
b'\xe3\x81\x8a'
b'\xe3\x81\x8b'

ただ、バイト列だと扱い辛いのでstr型に戻したいです。

最終形:UnicodeDecodeErrorが出る行を飛ばして処理する

出力をstrにしてUnicodeDecodeErrorが出る行をスキップする為には、decodeとtry・exceptを使います。最終形は下のコードです。

output_line.py
def main():
    with open("hoge.txt", 'rb') as f:  # 'r'でなく'rb'
        line = f.readline()
        while line:
            try:
                line = line.decode("utf-8")
            except UnicodeDecodeError:
                # utf-8でないバイト列が含まれる行はスキップする
                line = f.readline()
                continue
            # ...
            # 何か行いたい処理
            print(line.rstrip())
            # ...
            line = f.readline()

if __name__ == "__main__":
    main()
$ python output_line.py
あ
い
う
え
お
か

標準入力から受け取る場合

output_line.py
import sys

def main():
    for line in sys.stdin.buffer:  # bufferをつけるとバイト列で受け取れる
        try:
            line = line.decode("utf-8")
        except UnicodeDecodeError:
            # utf-8でないバイト列が含まれる行はスキップする
            continue
        ...
        # 何か行いたい処理
        print(line.rstrip())
        # ...

if __name__ == "__main__":
    main()
$ cat hoge.txt | python output_line.py
あ
い
う
え
お
か