Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Python で zip 展開(日本語ファイル名対応)

zip の中のファイル名

zip ファイル内にアーカイブされている各ファイルのファイル名エンコーディングは、(現バージョンだと) UTF-8 フラグの有無を指定することができるようですが、 UTF-8 以外のエンコーディングは指定することができません。

日本語ロケールの Windows で圧縮すると(ツールにもよりますが)、ファイル名は Shift_JIS (CP932) で書き込まれます。最近の Linux や Mac は UTF-8 がほとんどです。

異なる OS で圧縮された zip ファイルを展開するときに、 UTF-8 フラグが付いていれば問題なく展開することが可能ですが、 Shift_JIS (CP932) が使われている場合、ファイル名の文字化けが起きることがあります。

具体的に言うと Windows → Linux / Mac など。この場合は unar などのアーカイバを使ったり、 convmv などで化けたファイル名を修正する方法があります。

これとは別に Python の ZipFile ライブラリでも正しくファイル名を認識することができません。

Python3 の ZipFile ライブラリ

ZipFile ライブラリでは、 UTF-8 フラグがあれば UTF-8 として、そうでなければ CP437 として、バイト列を文字列に変換しています。

このため ZipFile.extractall() などを使って展開しようとすると日本語ファイル名が文字化けして展開されます。

対処するためには ZipInfo.filename を CP437 としてバイト列に戻したのち、正しいエンコーディングで文字列に戻し、それを ZipFile.extract(ZipInfo) とします。

import zipfile

f = r'/file/to/path'

with zipfile.ZipFile(f) as z:
    for info in z.infolist():
        info.filename = info.filename.encode('cp437').decode('cp932')
        z.extract(info)

上記は、本来のエンコーディングを CP932 と決め打ちで処理していますが、実際はそうとは限らないので、エンコーディング判定とか例外処理とかしたほうがよいです。

が…… 駄目っ……!

ZipFile を使って展開する処理を Mac とかで実行する場合は問題ないのですが、 Windows で展開しようとする場合は、ファイル名によってはエラーが発生します。

ZipInfo.filenameos.sep/ に置換されています。つまり Windows の場合 \ (\x5c) が / (\x2f) に置換されます。

CP437 は1バイト文字のエンコーディングで、 ASCII 印字文字( \x20 - \x7f )においては ASCII 互換ですので、(本来マルチバイト文字の一部であっても)この置換処理が行われてしまいます。その結果、一旦バイト列に戻すと b'\x90\x2f' といったパターンが出現してしまいます。

Shift_JIS (CP932) では2バイト目の \x2f は絶対に使用しないようになっているため、このようなバイト列を CP932 として再び文字列に変換しようとしたときにデコードエラーが発生してしまいます。

この問題が起きるのは、2バイト目が \x5c の文字。

そう。いわゆる「ダメ文字」です。(ダメな理由はこれとは別でしたが)

いやー。 Shift_JIS (CP932) のダメ文字とかここ10年くらい存在をすっかり忘れてました。しかも \x2f に置換したことで Shift_JIS (CP932) として不正なバイト列となるという変化球で来るとは。

解決法

置換処理が行われる前の( CP437 としてデコードされた)ファイル名情報は ZipInfo.orig_filename に格納されていますので、こっちを使うことで解決することができます。

import os
import zipfile

f = r'/file/to/path'

with zipfile.ZipFile(f) as z:
    for info in z.infolist():
        info.filename = info.orig_filename.encode('cp437').decode('cp932')
        if os.sep != "/" and os.sep in info.filename:
            info.filename = info.filename.replace(os.sep, "/")
        z.extract(info)
tohka383
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away