読者です 読者をやめる 読者になる 読者になる

Python3.1の Unicode ファイル名

パソコンを使う人なら誰でも"ファイル名"というのを知ってるし、プログラマなら誰でもファイル名を使ったファイル操作は基礎として学んでいる。しかし、21世紀の今日ですら、時としてファイル名とはやっかいな問題となりうるのだ。

Python3では、文字列オブジェクトとして Unicode を全面採用し、基本的にはファイルの操作も Unicode によるファイル名で行うようになった。 Windows のように、ファイルシステムUnicodeで構成されていれば問題はないが、Unix では Unicode だけでなく、日本語環境なら EUC-JP、ヨーロッパでは iso-8859-1 など、色々な文字コードによるファイル名が存在することになる。

たとえば、"a" という名前のファイルと "あ" という名前のファイルが置いてあるディレクトリがあるとする。この時、 "あ" のファイル名が UTF-8 で作成されていれば、

$ export LANG=ja_JP.UTF-8
$ ls
a あ

とすれば、ls コマンドでファイル一覧を表示することができる。しかし、ファイル名 "あ" が EUC-JP で記述されている場合は

$ export LANG=ja_JP.UTF-8
$ ls
?? a

と文字化けしてしまう。正しく表示するためには、

$ export LANG=ja_JP.EUC-JP
$ ls
a あ

のように、正しくロケールを設定しなければならない。

Python でファイル名を Unicodeとして扱う場合も、基本的には ls と同じようにロケールに従って変換される。まず Python2 での動作を確認してみよう。

$ python
>>> import os
>>> os.listdir(".")
['a', '\xe3\x81\x82']

このケースでは、 Unicode は一切使用していない。 os.listdir() は、OS から取得したファイル名をそのまま結果として返している。 '\xe3\x81\x82' は、UTF-8 で日本語の 'あ' である。

次に、ファイル名に Unicode を使ったケースを見てみよう。

$ export LANG=ja_JP.UTF-8
$ python
>>> import os
>>> os.listdir(u".")
[u'a', u'\u3042']

os.listdir()の引数として Unicode 文字列を渡すと、結果のファイル一覧も Unicode で返ってくる。 u'\u3042' は Unicode の'あ'であり、正しく Unicode に変換されているのがわかる。

では、ロケール設定が正しくない場合はどうなるだろうか?

$ export LANG=C
$ python
>>> import os
>>> os.listdir(u".")
[u'a', '\xe3\x81\x82']

ロケールを ASCIIのみをサポートする C として実行すると上記のような結果となった。ファイル名 'a' は Unicode に変換されているが、'あ' の方は Unicode ではなく、通常の文字列となってしまう。 'あ'はこのロケールでは変換できないため、始末に困った Python は OS から取得したファイル名を Unicode に変換せず、そのまま戻してしまうのである。

Python3 ではどのようになるだろうか? Python 3.1 からは PEP 383 Non-decodable Bytes in System Character Interfaces が実装され、正しく理解しておかなければ混乱を招きかねなくなっている。

まず、通常のケースを見てみよう。

$ export LANG=ja_JP.UTF-8
$ python3
>>> import os
>>> os.listdir(".")
['a', 'あ']

当然だが、ファイル名とロケールが一致している場合は全く問題ない。ファイル名は正しく Unicode で取得できる。

$ python3
>>> import os
>>> os.listdir(b".")
[b'a', b'\xe3\x81\x82']

ファイル名をバイト文字列で取得する場合も問題はない。 Unicode への変換は行われず、OS から取得した文字列がそのままバイト文字列として返される。

問題のファイル名とロケールが一致しないケースでは、次のようになる。

$ export LANG=C
$ python3
>>> import os
>>> os.listdir(".")
['a', '\udce3\udc81\udc82']

'\udce3\udc81\udc82' とはなんだろうか? Python 3.1 以降では、Unicode に変換できない文字はすべて "\udc80" - "\udcff" に変換されるようになったのである。Python から OS にファイル名を渡すときには逆の変換が行われるため、

>>> os.remove('\udce3\udc81\udc82')

とすれば ファイル名は b'\xe3\x81\x82' (=utf-8による'あ') に変換され、正しくファイルの削除を行うことができるという仕組みだ。

なお、"\udc80" - "\udcff" の領域は Unicode の定義ではサロゲートペアの下位ワードであり、他の文字とかぶることはない。しかし、処理によっては不正なサロゲートペアと判断され、なんらかの不具合が発生することも考えられる。

この変換はファイル名だけではなく、環境変数コマンドライン引数など、OSとの文字列の受け渡しで発生する。独自の拡張モジュールを作成する場合、OSや外部のライブラリと文字列をやりとりするなら同じ変換が必要となる場合があるだろう。