銀月の符号

Python 使い見習いの日記・雑記

tarfile と Shift-JIS ダメ文字

今週の日曜プログラミング、なににしようかな、と。かつての定番? アーカイブツール、圧縮解凍ツールの類にしよっと。 Python にて作るとどうなるのか試し始めた。 GUI 部分は最近使えるようになってきた wxPython にて Noah のような感じをイメージしつつ。対応形式は…標準ライブラリの力を借りるだけで扱えそうなのは zip, tar, gzip, bzip2 だから、まずはここから。しかし、意外なところで引っかかった。

本題へ。Python 2.5.2 の tarfile モジュール (version 0.8.0) を Windows 上で動かす場合、扱うファイル名に日本語が含まれるとうまく動かない。

tarfile モジュールを覗くと、これはマズい。 Unicode 文字列を意識して作られていないことが発覚。ついでに内部で os.path を使用している。os.path.dirname のようなパスを切る関数に 8bit 文字列を渡されては危険だ。

つまり、ファイル名を Shift-JIS で扱うと Windows のパス区切り、バックスラッシュで誤爆。なんというダメ文字の罠。だからといって安易に Unicode 文字列に逃げると内部で 8bit 文字列と混ざってエラー。 Unicode 文字一文字は 1 byte なんて前提が成り立つはずもなく、どこかで不整合が起こるのは想像できる。結果 add メソッドがディレクトリをたどったり、extract メソッドがファイルを取り出したりする際に問題となっているようだ。

いまさらながら tar 形式にてあつかうファイル名の文字エンコードにとくに規定がなくシステム依存というのは厳しい。なにかに統一されていれば楽なのだが tar の歴史を考えると無理な願い。

しかし自分の環境で作ったアーカイブが壊れているとか扱えない場合があるのは問題外。なんとかしないと。add の再帰探索を止めたり extractを extractfile に置き換えて、ここを自前で管理すればきちんと動くような気もするが…。

ここでふとひらめく。 Python3000 があるじゃないか、と。文字列は str 型(Python2 系列における unicode) 、外部とのやり取りやバイナリデータの扱いには byte 型ときちんと分かれているよりきれいになった Python。こちらでの tarfile モジュールは文字列とバイナリデータを分けることに成功しているはずだからこれをバックポートすれば…。

まずはホントに成功しているのかどうか Python3.0a5 のコードを確認。初期化時に sys.getfilesystemencoding を使いシステムを調べるようになっている。 Tarfile__init__ などのメソッドには encoding 引数が追加。tarfile のヘッダを作ったりする定数では byte 型を使用。たしかにファイルシステムの文字エンコードを意識するようになっていた!

#---------------------------------------------------------
# initialization
#---------------------------------------------------------
ENCODING = sys.getfilesystemencoding()
if ENCODING is None:
    ENCODING = "ascii"
class TarFile(object):
    """The TarFile Class provides an interface to tar archives.
    """
    # 中略
    encoding = ENCODING         # Encoding for 8-bit character strings.
    # 中略
    def __init__(self, name=None, mode="r", fileobj=None, format=None,
            tarinfo=None, dereference=None, ignore_zeros=None, encoding=None,
            errors=None, pax_headers=None, debug=None, errorlevel=None):
    # 中略
        if encoding is not None:
            self.encoding = encoding
    # 後略
#---------------------------------------------------------
# tar constants
#---------------------------------------------------------
NUL = b"\0"                     # the null character
BLOCKSIZE = 512                 # length of processing blocks
RECORDSIZE = BLOCKSIZE * 20     # length of records
GNU_MAGIC = b"ustar  \0"        # magic gnu tar string
POSIX_MAGIC = b"ustar\x0000"    # magic posix tar string
#後略

Python3.0a5 をインストール。実際に動作させて確認してみる。「ソ」などのダメ文字をつかってディレクトリ、ファイルをつくり Tarfile.add メソッドへ放り込む。Tarfile オブジェクトを for 文で回し格納されたファイル名を確認したり Tarfile.extract で取り出したり…。おぉ、問題なく動く。まだそんなに意地悪したわけではないので完璧かどうかまではわからないけど、「サシスセソ.txt」とか「一十百千万.txt」とか入れただけでおかしくなるのと比べれば巨大な進歩。

さて、この改良された tarfile モジュール… Python 2.5.2 で使うにあたって直す手間はそれなりにかかる。b"" リテラル、 "" リテラル、 print 関数、except 文の変化などなど。ここで再度ひらめく。2.5系で使うのが目的ならまずは Python2.6 を試した方がよかったんじゃ…。コードをちら見するとこちらも version 0.9.0 って書いてある。しょんぼり。後でこちらも調査…。