Python のファイル I/O API と文字コード
Mercurial で case 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 をバイト列に変換しようとして、『該当文字無し』と判断したためと思われる。
実は Cygwin 版 Python であれば、 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 化した Cygwin 版 Python であれば:
>>> f = file('\xc4\xb0', 'w'); f.write('a'); f.close()
>>> os.listdir(u'.')
[u'\uff84\uff70'] ※ 半角カタカナ
>>>
という結果になるし、LC_CTYPE 等でファイル名の文字コードを UTF-8 化した Cygwin 版 Python であれば:
>>> 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)