2007-03-04

雑談。というか自分用メモ。

標準入出力でファイルのエンコーディングを認識した処理をする

以下に出てくるコードは Python のものですが、問題自体はどの言語で書いたスクリプトでも同じはず。

スクリプトでテキストフィルタなんかを書くとき、ファイルのエンコーディングを認識させて処理したいわけです。いまどき危険な文字があるから文字コードは EUC でとか UTF-8 でとかダサいことを言いたくないわけです(そもそもそれを選べないこともある)。そうするとコマンドラインオプションかなんかを作って、こんなふうに使うものを作ることになります。

foo.py -e shift_jis < in.txt > out.txt

foo.py の中身では各行を読み込んで、line.decode(encoding) などとしてから本処理にかかるわけです。

ところがこれはうまくいかないことがある。in.txt がリトルエンディアンの UTF-16 だった場合です。(UTF-32 でも同様の問題が起きますが、理屈や解決法は同じことです。)

リトルエンディアンの UTF-16 では、改行 (LF) は 0x0a 0x00 というバイト列で表現されます。このため、エンコーディングを認識しないファイルオブジェクトで――つまり普通に file(filename) で開いただけのファイルはどれでも――、 readline() などを実行すると、この 2 バイトの前半分のところでデータがちょん切られることになります(自分でテスト用にスクリプトを書いてやってみてくださいな)。残りの後ろ半分の 0x00 は次の行の頭にくっついてきます。こうした壊れたデータに対して decode() メソッドを実行すると、例外が発生したりおかしなデータが返ってきたりします(挙動は decode() メソッドの第 2 引数 errors の指定によって変わる)。

だから UTF-16 はタコでそんなもん使うなとも言えるわけですが、まあそれはそれとして対策を考えてみます。

簡単なのは、readline() なんてせこせこしたことはやらずに、ぜんぶ読み込んで(data = file(filename).read())、これに対してまとめて decode() する。上に書いたような問題があることに気づいてなくても、内部でこのように処理をしているスクリプトではそもそもこの問題は起きません(じつは後述するべつの問題は残っている)。メモリの潤沢な昨今、たいていの用途ではこうしたほうが処理自体も早く終わることが多いはず。

しかしつねにそれができるとは限らない。データベースからエクスポートしたものすごくでかいファイルを処理するときとか、別のコマンドが吐き続ける出力をパイプで変換し続けるとか、逐次読み込み、逐次変換、逐次書き出しという作りが要求されることもあります。

(標準入出力のように)すでに開いているファイルオブジェクトに対して、エンコーディングを認識したインターフェースでアクセスしたい――そういうときには codecs モジュールStreamReaderWriter クラスを使います。

例は標準ライブラリの codecs.pyopen 関数のソースを見てもらうのが早いのですが(10行もないので)、こんなふうにします。

encoding = 'utf-16-le'

(enc, dec, sr, sw) = codecs.lookup(encoding) # 今回は enc, dec に用はないけど
fp = codecs.StreamReaderWriter(sys.stdin, sr, sw, errors)

# 以下 fp に対して普通のファイルオブジェクトとして処理
for line in fp:
    do_something(line) # line は unicode オブジェクトになっている

これでリトルエンディアンの UTF-16 のテキストファイルをリダイレクトされても大丈夫です。

めでたしめでたし、ではないのです。スクリプトの出力をよく見るとわかるのですが、じつはまだ問題がふたつあります(ヒント: うちひとつは Windows のみで起こる)。これについてはまたこんど書きます。