2007-03-07

前回からの続き。

前回の話は、まとめると、テキストファイルをエンコーディングの判定する前に行分けすると UTF-16LE の時にハマるよ、ということで、これはどのプログラムでも共通の話題なのですが、ここからは Python 固有の話です。

ところが調べているうちにちょっと状況というか視点が変わってきちゃいまして……。以下に述べるこれは、codecs モジュールのバグなのではないかという気がしてきたのですが、どうでしょう。清水川さん見てるかなあ。識者の意見がほしいです。

とりあえず、前回「ふたつ問題が」と書いたのは、以下のことを想定していました。

  • ファイル先頭に BOM が付いていることがある(から気をつけたし)。
  • codecs.open で開いたファイルオブジェクトは改行コードが変換されない。

ひとつ目は読んでそのままの内容なのですが、自前で判定するのは難しくないですし、そもそもどうやら Python 2.5 で BOM 付きの codec が追加されたようなのでそんなに問題ではなくなってしまいました。(これを検証するためにようやく 2.5 を入れたのです。)

で、もう一点ですが、codecs.opencodecs.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? それは度胸がいるなぁ……。

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 のみで起こる)。これについてはまたこんど書きます。

2007-02-22

ストールマンの翻訳でプロジェクト杉田玄白に入れてもらおうと、山形浩生氏にメールを出してあったのだけれど、お返事をいただけないまま、氏はガーナに旅立たれてしまわれた。

たんに積まれているだけならいいのだけれど(「使える GUI デザイン」のときもリストに載ったのはしばらく経ってからだった――でも返事はすぐきてたな)、スパムとスパムフィルタのせいで、最近はほんとに目にとまってないということもじゅうぶんありえるからなあ(同時に自分が返信を見落としていないかも気をつけないと)。こういうときどうしたらいいのか。

2007-02-01

御大です。わお。

社会の問題について、「そもそも」から考え直すよう迫る人は、アメリカでは無政府主義者などとみなされ危険視されたりするようですが、この国ではめんどくさい人として鬱陶しがられるだけでとりあってもらえずに終わる。

周囲の人に署名を頼めば、みんな喜んで名前を書いてくれるだろうけど、かれらがそうする理由は、趣旨に賛同するからじゃなくて、依頼人のあなたがお友達(あるいは家族)だからだ。説明したところで、「なるほど、勉強しているね」で、半日後にはなにについて署名したかも思い出せなくなるのがふつう。

あることについて、だれかに「その人自身の意見を持たせる」ところまで持っていかせるのは難しい。話を聞いてもらうのより難しい。脳はカロリーをたくさん消費するらしいので、だから人間は考えさせられるのを極力避けるのかもしれない。

今回の短いエッセイについても、何人かが「はてなブックマーク」に入れたりして、当座の読みものとして一部の人の興を満たして終わるだけなのかもしれません。しかしそういう人たちはこの話題についてはもう相当の議論に目を通している面々というような気がします。つまりあんまり意味ないのではないかと。

また英語圏には英語圏流の理屈の進めかたというのがあるので、これがそのまま日本で「知的財産」について考える入門としては向かないのかもしれないし、だれか詳しい人がそういうやつの「日本版」を書いてくれたりしたらいいのになあ、と思ったりもします。

そうはいっても、これが発表されてから二年たつというのにいまだに翻訳されていなかったというのは、ちょっと意外でした。なにもないよりは、少しでも議論の踏み台があったほうがいいだろうとも思いますし、そういうわけで……。

更新情報

リチャード・M・ストールマン (Richard M. Stallman) さんの Did You Say “Intellectual Property”? It’s a Seductive Mirage の日本語訳、「『知的財産』だって? そいつは砂上の楼閣だ」を公開します。公開前にレビューをしてくれた友人に感謝。

2007-01-27

雑談というか告知というか。

「さくら Python」で改訂したいところ

『さくらのレンタルサーバ』で Python 外部モジュールを使う」が参考になっている方々が少なからずいるようで、書いた者としてはやってよかったと思っています。(べつにページの有用さ具合を測る尺度にはならないけど)はてなブックマークに入れている人もいるし、web.py (作ったのは Aaron Swartz だ)を導入したり、Trac を動かす(!)などの参考文献として参照してもらってるようです。

ただ、そこで書いた user モジュールを使って sys.path をいじる方法を気に入ってしまった人が思いのほか多いようなのが心配になってきた。あのやり方はよくないと思います。

あの節を書いている時点では、なんとなく、直感的に、「これはどうなのか」というサインが頭の片隅に浮かんでいただけの状態で、理論的にそれを説明することには及ばなかったのでそのままタイプを進めてしまったのですが(その感触があったせいで、今の版でもそれほどプッシュはしていない)。その後、考えも整理できてきた今では、はっきりと「これはよくない」と考えています。改訂したら、この箇所は削除するか、ちょっと触れる程度にしたい。

なぜか? CGI に限らず、プログラミングをよくしている人なら当然すぐにそう考えたはずのことなのですが……。文章にすると時間がかかるので(そしてそれをやるならここじゃなくて「さくら Python」の記事でやればいいわけで)、以下思いつくままに箇条書き。

  • プログラム(ここでは CGI スクリプト)を書くときに意識しなければいけないこととして、どこまで可搬性(ポータビリティ)を持たせるか(移植性、といってもいい)、ということがある。
  • 自分しか使わないスクリプトなら、ファイルのパスでもログインパスワードでも、スクリプトにハードコードしてかまわない。
  • しかしこうしたスクリプトはよそで動かす(便利なものは遅かれ早かれそうしたくなる)ときにコードを書き換えなくてはいけない(「さくら Python」の当該節も文脈はそういう流れ)。
  • そこでプログラマは環境に依存しない部分と、状況に応じて変更する必要がある部分とを分ける。前者をモジュールにしたり、後者を設定ファイルにしたりする。
  • 可搬性の境界ははっきりしておいたほうがよい。つまり、「このファイル群は何も考えずによその環境へうつせる」し、よその環境へ移したら「あのファイル群(だけ)を書き換えればよい」という状況にしておくべきだ。
  • つまり環境依存部分は「分ける」のが肝心なので「隠す」のはまったくよくない。
  • sys.path を追加するのはもちろん環境依存のプライベートな話。つまりこれが書いてあるファイルは環境依存のファイルということになる。
  • しかしある CGI スクリプト foo.cgi からそれを追い出して、代わりに import user したところで、foo.cgi に可搬性ができたといえるのか。いえない。なぜなら import user とはプライベートな用途の最たるもので、これはモジュールのパスを追加するための仕組みでもなければ、CGI のための仕組みですらないimport user してるスクリプトというのはプライベートの領域に属するファイルとみなすべき。
  • .pythonrc.py で何が行われるのか、import user している側からは、それは想像もつかない。自前の関数で range をオーバーロードしてるかもしれない。
  • どうせスクリプトがプライベートなままなのなら、sys.path.append(...) と直接書いても移植性は(移植性がないことに)変わりないし、むしろそこで何をすべきかがわかってよい。はじめから「これは環境によって書き換えるファイル」と考えておけば、sys.path が通ってないから云々という問題に悩まされることはかえって少なくなる。
  • ほんとうに foo.cgi に可搬性を持たせたいなら、設定ファイルを作って、それを読み込むようにするべき。これで foo.cgi はポータブルになったけど、総ファイル数は増えている(のでよく考えよう)。

じっさいに改訂したら、こんな理由までこまごまとは書かないと思いますけど、まあ考えの経緯はこんな感じです……。最初に感じてた「どうなのか」サインを、記事を読んだ人も持ってくれるかなあ、というのをあてにしてたということもあったのかもしれない。そもそも import user じたいが暗黙的で Python らしくないですよね。

ここに書いたって読む人はほとんどいないから、一刻も早く改訂しないと……。