彷徨えるフジワラ

年がら年中さまよってます

Python のファイル I/O API と文字コード

Mercurialcase insensitive filesystem 周りの修正をする際に仕込んだ知識のまとめエントリその3。でも case insensitive とは関係の無い話を。

今回の作業で知ったのだけれど、Python のファイル I/O 系 API では、引数が Unicode かバイト列かに応じて、戻り値も Unicode かバイト列かが切り替わる。これって入門書とかに載ってるのかな?だとしたら全然気が付かなかった .... orz

例えば "os.listdir(u'.')" の戻り値配列は Unicode オブジェクトを格納しているけれど、同じメソッドの起動でも "os.listdir('.')" であれば戻り値配列に格納されているのはバイト列形式、という具合。

Windows ネイティブ版であれば、内部的にWin32 API の切り替えを行っているのかもしれないけれど、プラットフォーム固有コードの低減という点で考えれば、Unicode ⇔ バイト列変換は Python 側で実施している可能性も。性能的な観点から『ネイティブ側で実施できる分はネイティブ側で実施』という判断もありだけれど、Python 本体の実装って、どっちの方針なんだろ? > 共通コード最大化/ネイティブ実行重視

さて、日本語 Windows 上でトルコ語アルファベットの U+0130 を名前に使ったファイルを生成した場合:

$ python -i
Python ....
>>> f = file(u'\u0130', 'w'); f.write('a'); f.close()
>>> import os
>>> os.listdir(u'.')
[u'\u0130']
>>> os.listdir('.')
['?']
>>>

上記のように、返却値が Unicode 形式の場合は妥当な値が返って来るけれど、バイト列形式の場合は "?" が返って来る。
これは Windows ネイティブ版の Python が、システムのデフォルト文字コード設定 (日本語環境なら cp932) に従って U+0130 をバイト列に変換しようとして、『該当文字無し』と判断したためと思われる。

実は CygwinPython であれば、 LC_CTYPE 等のロケール設定によってファイル名の文字コードを制御することが可能なので:

$ (export LC_CTYPE=ja_JP.utf-8; python -i)
Python ....
>>> f = file(u'\u0130', 'w'); f.write('a'); f.close()
>>> import os
>>> os.listdir('.')
['\xc4\xb0']
>>>

といった具合に、バイト列形式でも適切な結果を得ることができる。但し、戻り値のバイト列は当然のことながら UTF-8 形式。

これまでの例では、生成対象ファイルのファイル名指定に Unicode 形式を使用してきたけれど、バイト列形式でのファイル名指定ならどうなるか?

"\xc4\xb0" というバイト列は、世界的に見たら『U+0130 の UTF-8 形式』と想定するのが一般的なのだろうけれど、実は半角カタカナ2文字『トー』(U+FF84 U+FF70) の cp932 形式としても正当なバイト列でもある。

なので、Windows ネイティブ版の Python を日本語 Windows 環境で稼動させた場合や、LC_CTYPE 等でファイル名文字コードを cp932 化した CygwinPython であれば:

>>> f = file('\xc4\xb0', 'w'); f.write('a'); f.close()
>>> os.listdir(u'.')
[u'\uff84\uff70'] ※ 半角カタカナ
>>>

という結果になるし、LC_CTYPE 等でファイル名の文字コードUTF-8 化した CygwinPython であれば:

>>> f = file('\xc4\xb0', 'w'); f.write('a'); f.close()
>>> os.listdir(u'.')
[u'\u0130']トルコ語
>>> 

という結果になる。

Python のファイル I/O API をバイト列形式で使うのは意外におっかねぇーなーという話(笑)。

ちなみに、手元の Linux 環境の Python (2.6.6) では、LC_CTYPE で UTF-8 指定した場合の挙動は期待通りなのだけれど、cp932 を設定した場合は期待通りに動作しない。

>>> os.listdir(u'.')
['\xc4\xb0'] ※ 何故かバイト列の戻り値
>>>

ここで、想定される Python の内部処理を順を追って確認してみる。

LC_CTYPE で cp932 を設定した場合、locale.getpreferredencoding() を使ってデフォルト文字コードを確認すると、'WINDOWS-31J' が返って来る。

$ (export LC_CTYPE=ja_JP.cp932; python -i)
Python ....
>>> import locale
>>> locale.getpreferredencoding()
'WINDOWS-31J'
>>> 

'WINDOWS-31J' は Shift-JIS の IANA 正式登録名なので、『あぁ、内部的には cp932 と WINDOWS-31J は同じ扱いなのね』と思っていたのだが:

>>> '\xc4\xb0'.decode(locale.getpreferredencoding())
Traceback (most recent call last):
  File "", line 1, in 
LookupError: unknown encoding: WINDOWS-31J
>>>

はぁ?!なんじゃそりゃ? > unknown encoding

"cp932" を直接指定すればバイト列 ⇒ Unicode の decode 処理は正常終了するので、先述した "os.listdir(u'.')" の戻り値がバイト列になってしまうのは、ファイル名の Unicode 化の際に WINDOWS-31J による変換が失敗するのが原因と思われる。

とは言っても、ロケール環境変数設定値の cp932 から WINDOWS-31J を引き当てているのに、なんで WINDOWS-31J から符号化処理を引き当てられないのかね?ひょっとして iconv とかの追加ライブラリのインストールが必要とか? > Python (2.6.6)