テスト。
2007-03-18
テスト。
Masaaki Shibata blog
テスト。
雑談。
いつの間にか、ThinkPad (X60s) が Intel VT に対応してた。
BIOS で VT を Enable にしたあと、一旦電源を切らないと有効にならないので注意(再起動ではだめ)。
前回からの続き。
前回の話は、まとめると、現状では、codecs.open で開いたファイルはプラットフォームごとの改行コードの違いを吸収してくれない、ということでした。
しかたないので、codecs モジュールで使われているクラスを継承して、間に入って改行コードの変換をやらせてみることにしました。こんな感じで……。
class StreamReaderWriter(codecs.StreamReaderWriter): def __init__(self, stream, Reader, Writer, errors, linesep=os.linesep): codecs.StreamReaderWriter.__init__(self, stream, Reader, Writer, errors) self.linesep = linesep def read(self, size=-1): return self.reader.read(size).replace(self.linesep, '¥n') def readline(self, size=None): return self.reader.readline(size).replace(self.linesep, '¥n') def readlines(self, sizehint=None): return [line.replace(self.linesep, '¥n') for line in self.reader.readlines(sizehint)] def next(self): return self.reader.next().replace(self.linesep, '¥n') def write(self, data): return self.writer.write(data.replace('¥n', self.linesep)) def writelines(self, list): return self.writer.writelines([line.replace('¥n', self.linesep) for line in list]) def interface(fp, encoding, errors='strict', linesep=os.linesep): reader, writer = codecs.lookup(encoding)[2:] return StreamReaderWriter(fp, reader, writer, errors, linesep) def open(filename, mode, encoding=None, errors='strict', bufsize=1, linesep=os.linesep): if not encoding: fp = file(filename, mode, bufsize) else: if 'b' not in mode: mode += 'b' fp = interface(file(filename, mode, bufsize), encoding, errors, linesep) fp.encoding = encoding return fp
まだ実運用はしてないので使いものになるかどうかはわからないけど、簡単にテストした限りではこれでよさそうです。
前回からの続き。
前回の話は、まとめると、テキストファイルをエンコーディングの判定する前に行分けすると UTF-16LE の時にハマるよ、ということで、これはどのプログラムでも共通の話題なのですが、ここからは Python 固有の話です。
ところが調べているうちにちょっと状況というか視点が変わってきちゃいまして……。以下に述べるこれは、codecs モジュールのバグなのではないかという気がしてきたのですが、どうでしょう。清水川さん見てるかなあ。識者の意見がほしいです。
とりあえず、前回「ふたつ問題が」と書いたのは、以下のことを想定していました。
ひとつ目は読んでそのままの内容なのですが、自前で判定するのは難しくないですし、そもそもどうやら Python 2.5 で BOM 付きの codec が追加されたようなのでそんなに問題ではなくなってしまいました。(これを検証するためにようやく 2.5 を入れたのです。)
で、もう一点ですが、codecs.open や codecs.StreamReaderWriter で作ったファイルオブジェクトは、内部的には必要上――前回書いたようなことからくる理由――からファイルをバイナリモードでオープンしているようなのですが、にもかかわらずテキストモードに期待される OS ごとの改行コードの変換処理を肩代わりしていないように思われます。
このことはテキストモード/バイナリモードの区別がない Linux や FreeBSD 等では問題にならないのですが、Windows では困ったことが起こります。
検証。まず以下のようなテキストファイル a.txt を作り、エンコーディング Shift_JIS(なんでもいいですが)、改行コード CRLF で保存する。
abc あいう
Python インタプリタを起動。
C:¥Documents and Settings¥mshibata¥My Documents>python Python 2.5 (r25:51908, Sep 19 2006, 09:52:17) [MSC v.1310 32 bit (Intel)] on w 32 Type "help", "copyright", "credits" or "license" for more information. >>> file('a.txt', 'rb').read() 'abc¥r¥n¥x82¥xa0¥x82¥xa2¥x82¥xa4¥r¥n' >>> file('a.txt', 'r').read() 'abc¥n¥x82¥xa0¥x82¥xa2¥x82¥xa4¥n' >>> file('a.txt', 'r').read().decode('shift_jis') u'abc¥n¥u3042¥u3044¥u3046¥n' >>> import codecs >>> codecs.open('a.txt', 'r', 'shift_jis').read() u'abc¥r¥n¥u3042¥u3044¥u3046¥r¥n' >>>
ほらあ! やっぱりおかしい。これとは逆に書き込みでは、codecs.open('b.txt', 'w', 'shift_jis').write('hoge hoge hoge¥n')
とすると Windows では当然改行コードが ‘¥r¥n’ に置き換えられることを期待するが、それは行われず、結果的に改行コード LF のテキストファイルができてしまう。
“codecs” をキーワードに Python Bug Tracker を検索してみたけど、ステータスが Open になって残っている中ではそれらしい議論は見当たらず。Closed の中には前回書いたようなことと思しき議論はあったけど、それ自体はもう解決しているので今回の話とは違うし。
comp.lang.python には、「codecs.open はモード ‘rU’ で開くとなんか変だよね」というような話(たぶん)が見つかったけど、「本来やるべき改行コードの変換をしてない」という視点には行ってないように見えます。
だいたい本来エンコーディングを自前で実装するなら手順的には「読み込み→デコード→改行コード変換」とするべきなのに、歴史的経緯から改行コードの変換は C の fopen に頼ってるもんだから(すみませんソース見たわけじゃないですけどたぶん)、「(読み込み→改行コード変換)→デコード」(括弧内が fopen の管轄)になってしまっているのが悪い。
と、いったような意見に至ったのですが、どうなんでしょう。それにどこに言えばいいんだ? comp.lang.python? それは度胸がいるなぁ……。
雑談。というか自分用メモ。
以下に出てくるコードは 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.py の open 関数のソースを見てもらうのが早いのですが(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 のみで起こる)。これについてはまたこんど書きます。