Pythonのcsv
パッケージは大変便利です。面倒なエスケープ処理をちゃんと行ってくれます。とりわけ、Excelファイルで送られてきたファイルを処理するのに重宝します。なんといっても、dialect='excel'
でExcel CSVをちゃんと読めます。
ところが、日本語が絡むと問題は途端に厄介になります。問題の本質は何か。まず、ExcelのTSVの扱いに関して、以下のことが知られています。
- Excelのtsvエクスポートはutf-16(BOM付き)である
- Excelが直接読めるのはBOM付きutf-16のtsvである(カンマ区切りはウィザードが必要)
- csv(カンマ区切り)出力や読み込みも可能だがおすすめできない(後述)
このことからわかるのは、BOM付きutf-16ファイルを扱うのが最善ということです。これを扱う一番の方法は、io
パッケージを使うことです。
with io.open(path, encoding='utf-16') as f:
...
さらっと書きましたが、io
パッケージを使うのが実は肝です。utf-16はそこそこに扱いにくいフォーマットです。まず、BOMがいます。さらにこの状態でcr+lf
されると、バイト単位では間に\0
が挟まるということです。ですので、普通にopen()
を使うと謎の挙動をします。
さてここで問題が起こります。python2系を使っていると、csv
パッケージはunicode
をサポートしていません。従って、上の方法で開くと読めません。ここで、日和ってopen
を使うと、上記の理由で読むのに苦労します。以前は私はshift-jisのcsvでエクスポートしていたんですが、Excelは内部的にUnicodeのようで、濁点が別文字になった正規化されてない状態のことがあり、このときshift-jis出力したら濁点が全部消失しました。なのでこの方法もおすすめしません。
実はUnicodeをcsv
で読む方法は、マニュアルの最後に書いてあります。正解は、unicode
のストリームを一旦utf-8にエンコードして、この状態でcsv
パッケージで読ませて、スプリットしたら再度utf-8をデコードすればよいのです。書き込む時も同じで、いったんunicode
をutf-8にしてStringIO
に書き込んで、ファイルに吐き出す直前で再度unicode
に変換すればいいのです。圧倒的に2度手間ですが、長年Excelと格闘して最終的に一番安定した出力が得られませいた。ちなみに、Python3ではcsv
パッケージがstr
(2で言うところのunicode
)をサポートしたので、もはや普通にio
で開いて処理すればいいです。
流石にこれらの処理を毎回書いていると日が暮れるので、ライブラリにしました。
上記の問題を全て回避しています、たぶん。
やってはいけないこと
以上が全てですが、おさらい的にやってはいけないことと理由を羅列してみましょう。
要素ごとにutf-16でencode
.encode('utf-16')
はBOM付きutf-16文字列を生成します。ここが、その他のエンコードとの決定的な違いです。
>>> u'a'.encode('utf-16')
'\xff\xfea\x00'
[x.encode('utf-16') for x in row]
の様なコードを書くと、アチラコチラにBOMが入った謎の文字列が生成されしまいます。このアプローチはダメです。ファイルの先頭にだけBOMが入るようにしないといけません。
これを回避する一番よい方法は、原則的にunicode
で扱って、ファイル出力の部分で制御することです。これをやってくれるのがio
パッケージであり、通常のopen
を使って自前でエンコードするべきではありません。
別の回避策として、一旦StringIO
にutf-8で出力しておいて、ファイルに書き込む前に全体をunicode
に変換してから、再度utf-16に変換し直すという方法もあります。が、io
を使ったほうがすっきりします。
openで開いてパースしてからデコード
utf-16ということは、パースしたい改行文字やタブ文字も2バイトで書かれているということです。1バイト単位で処理するPython2のcsv
パッケージに処理させると、不要な\0
文字が残ってしまいます。このアプローチが使えるのは、Ascii文字がそのまま残っているutf-8やshift-jisなどだけです。
shift-jisのcsvでエクスポートしてから読み込み
エクセルは内部的にはUnicodeでデータを持っているようです。最悪なのは、濁点が別文字として保存されていることがあり、この状態でshift-jisでエクスポートすると、濁点が全部消えます。正規化してくれればいいのに。そのため、excelからのエクスポートはutf-16テキスト一択になります。扱いづらいですがしかたありません。