pythonのzipfileモジュールがだめ文字をうまく扱ってくれないのを解決する

zipfileモジュールが駄目文字をうまく扱えない

windowsで、フォルダを右クリック→送る→圧縮(zip)フォルダで作成したzipファイルをzipfileモジュールで正しく解凍できないケースに遭遇した。

原因がわからず調べてみたら、どうやらsjisのだめ文字というのの扱いがうまくないらしい。

どういう動作になるのか、実際に以下のエントリを持つzipアーカイブを用意して試してみた。

解凍に使うコードはこんな感じ。

import zipfile
ar = zipfile.ZipFile('test.zip')
ar.extractall()

python2.6で実行すると、

?!
IOErrorを吐きつつこんな結果に。


マルチバイト文字列の扱いの問題なら、デフォルトの文字列クラスがunicodeになったpython3ならうまく扱えるかもしれない、とpython3.2でも試してみると・・・
WindowsErrorを吐きつつこんな結果に.

:-O

結果としては、python2.6でいくつかのメソッドを上書きする事でうまく解凍できるようになった。

python2.6で以下のモジュールをインポートすれば、パス文字列を内部でunicodeで扱う代替zipfileモジュールのようなものが使えるようになる。


なぜ正しく解凍できないのか

つらつらとコードを眺めた結果、うまく処理されない理由は

zipfile.ZipInfoとzipfile.ZipFileでファイル名をバイト列として扱っている

ため。

誤った正規化

ZipFile.__init__でファイル名のバイト列中にある文字、os.sepをスラッシュに正規化している。この処理でsjisの2バイト文字のバックスラッシュがWindowsのパス区切りと誤認されて全てスラッシュに置き換えられてしまう。結果、解凍されるフォルダ名が文字化けする。

       # This is used to ensure paths in generated ZIP files always use
       # forward slashes as the directory separator, as required by the
       # ZIP format specification.
       if os.sep != "/" and os.sep in filename:
           filename = filename.replace(os.sep, "/")
os.makedirsの誤った分割

zipfile.ZipFile.extractは内部でos.makedirs(unixでいうmake-pのような関数)を呼び出す。この関数は、パス文字列をos.sepで分割して、未だ存在しないフォルダを作成していく。このとき、1.で正規化されたパス文字列が/で分割され、誤ったパスでディレクトリが作成されていく。


さらに、この記事を書くために動作を追いかけたら、zipfile.ZipFileクラスはファイルを含まないディレクトリを正しく展開できないことも見つけた。
規格でどうなってるか知らんけどとりあえずwindowsで「送る」からzipを作ると末尾がパス区切りのエントリが来ちゃうので処理できないと困る。
というかtmpディレクトリとか、プログラムでzipを生成するとかいうシチュエーションを考えると規格で空ディレクトリが扱えないってことはないんじゃないかなあとか。
ちなみにpython3.2だと空のディレクトリを含んでいても正しく展開できた。上のプログラムでは、空のディレクトリは展開しない。