Python WebAcademy Blog

Pythonのzipfileとは?ZIPの作成・中身の確認・安全な展開までを初心者向けに解説

|

毎月のレポートをZIPにまとめる作業、手でやっていませんか。標準ライブラリのzipfileを使えば、ZIPの作成も中身の確認も展開もPythonから自動化できます。ZIP_DEFLATEDを付け忘れると圧縮されない理由、展開せずにCSVを読む方法、Windowsで作られたZIPの文字化け対策、信頼できないZIPを安全に展開する書き方まで、実際に動かした結果を見ながら初心者向けに解説します。

取引先から届いたZIPを開いて、中のCSVを取り出して、集計する。そんな作業を毎月くり返していませんか。

逆に、作ったレポートをフォルダごとZIPにまとめて送る作業もよくあります。1回なら右クリックで済みますが、毎週となると地味に時間を取られます。

こうした作業は、Python標準ライブラリのzipfileで自動化できます。pip installは要りません。

今回はZIPの作成、中身の確認、展開に加えて、初心者がつまずきやすい圧縮の設定と文字化け、そして安全な展開の書き方まで順番に見ていきます。

zipfileは、ZIPを読み書きするための標準ライブラリ

ZIPは、複数のファイルを1つにまとめて、サイズを小さくするための形式です。メールの添付やダウンロード配布でおなじみですね。

zipfileは、このZIPをPythonから作ったり、読んだり、追記したりするためのモジュールです。import zipfileと書けば、すぐに使えます。

公式ドキュメントによると、4GiBを超える大きなZIPにも対応しています。一方で、暗号化されたZIPの展開はできても、暗号化したZIPを新しく作ることはできません。

ちなみに、フォルダを丸ごとZIPにするだけなら、shutilのmake_archiveのほうが手軽です。こちらで紹介しています。【関連記事】Pythonのshutilとは?ファイルのコピーや移動、zip圧縮を自動化する基本を初心者向けに解説

それでもzipfileを使うのは、ZIPの中身を1ファイルずつ細かく扱えるからです。

まず覚えたい4つのモード

zipfileの中心はZipFileクラスです。ファイルを開くときと同じように、モードを指定して開きます。

モードは次の4つです。ファイルの読み書きで使うopen()とよく似ています。

モード 意味 既存のZIPがあるとき
"r" 読み込み(省略時はこれ) そのまま読む
"w" 新しく書き込む 中身を消して作り直す
"a" 追記する 末尾にファイルを足す
"x" 新しく作るときだけ書き込む FileExistsErrorになる

いちばん気をつけたいのは"w"です。同じ名前のZIPがあると、警告なしに中身が消えます。

うっかり上書きを防ぎたい場面では、"x"を選ぶと安心です。モードの考え方そのものに自信がない方は、先にこちらを読んでおくと理解が早くなります。【関連記事】Pythonのファイル読み書きとは?open()とwith文の基本を初心者向けに解説

ZIPを作るときは、圧縮方式の指定を忘れない

それでは、実際にZIPを作ってみましょう。例として、売上のCSVとメモが入ったreportフォルダを、まるごとZIPにします。

フォルダの中を順番にたどるために、pathlibのrglobを使っています。

import zipfile
from pathlib import Path

report = Path("report")

with zipfile.ZipFile("report.zip", "w", compression=zipfile.ZIP_DEFLATED) as zf:
    for path in sorted(report.rglob("*")):
        # ZIPの中での名前を report/〜 の相対パスにそろえる
        zf.write(path, arcname=path.relative_to(report.parent))

with zipfile.ZipFile("report.zip") as zf:
    for info in zf.infolist():
        print(info.filename, info.file_size, info.compress_size)

手元のPython 3.11で動かすと、次のように表示されました。左から、ZIPの中での名前、元のサイズ、圧縮後のサイズです。

report/sales.csv 32012 107
report/メモ.txt 29 34

CSVは32012バイトが107バイトまで縮みました。同じ行をくり返しただけのテストデータなので極端に縮んでいますが、圧縮が効いていることはわかります。

with文で開いているので、ブロックを抜けると自動で閉じられます。公式ドキュメントには、閉じ忘れると必要な情報が書き込まれないと明記されているので、withで書くのが基本です。

compressionを省略すると、圧縮されない

ここが、初心者がいちばん引っかかるポイントです。compressionを省略すると、既定値のZIP_STOREDになります。

STOREDは、そのまま保存するという意味です。つまりファイルを1つにまとめるだけで、サイズはまったく小さくなりません。

実際にcompressionを付けずに同じCSVを入れてみると、圧縮後のサイズも32012バイトのままでした。

私は10年ほどエンジニアとして開発に関わってきましたが、まさにこれで恥をかいたことがあります。ログを毎晩ZIPにしてサーバーの容量を節約するバッチを作ったのに、何か月たっても容量が減らない。原因はZIP_DEFLATEDの書き忘れで、まとめただけのファイルが毎晩増えていました。

選べる圧縮方式は、次のとおりです。迷ったらZIP_DEFLATEDを選べば、ほとんどの環境で開けます。

定数 方式 ひとこと
ZIP_STORED 圧縮しない 既定値。まとめるだけ
ZIP_DEFLATED Deflate もっとも一般的。迷ったらこれ
ZIP_BZIP2 bzip2 縮みやすいが対応ツールが少なめ
ZIP_LZMA LZMA 同上
ZIP_ZSTANDARD Zstandard Python 3.14で追加

DEFLATED以外の方式は、古いツールや古いPythonでは開けないことがあると、公式ドキュメントでも注意されています。相手の環境がわからないときは、DEFLATEDにしておくのが無難です。

arcnameで、ZIPの中の名前を決める

write()の第2引数arcnameは、ZIPの中でどんな名前にするかを決めるものです。

これを省略すると、渡したパスがそのまま使われます。たとえば/home/user/work/report/sales.csvを渡すと、展開したときに深いフォルダが何段もできてしまいます。

受け取った人が開いたときの見え方を想像して、arcnameを決めましょう。

展開せずに、中身を確認して読む

ZIPを受け取ったとき、いきなり全部を展開する必要はありません。まずは何が入っているのかを確認しましょう。

ファイル名の一覧はnamelist()、サイズなどの詳しい情報はinfolist()で取れます。

with zipfile.ZipFile("report.zip") as zf:
    print(zf.namelist())
    print(zf.read("report/メモ.txt").decode("utf-8"))

実行すると、次のように表示されます。read()はバイト列を返すので、文字列にするにはdecode()が必要です。

['report/sales.csv', 'report/メモ.txt']
9月の売上メモです。

CSVをZIPに入れたまま集計する

大きなCSVの場合、展開してから読むと、ディスクに同じデータがもう1つできてしまいます。open()を使えば、ZIPの中のファイルを直接読めます。

ただしopen()で得られるのはバイト列の流れです。文字列として読むために、io.TextIOWrapperで包みます。

import csv
import io
import zipfile

with zipfile.ZipFile("report.zip") as zf:
    with zf.open("report/sales.csv") as f:
        reader = csv.DictReader(io.TextIOWrapper(f, encoding="utf-8"))
        total = sum(int(row["amount"]) for row in reader)

print(total)  # 2400000

展開用の一時フォルダを用意する必要も、後片付けをする必要もありません。CSVの読み方そのものは、こちらで詳しく解説しています。【関連記事】Pythonのcsvモジュールとは?表データの読み書きと文字化けの防ぎ方を初心者向けに解説

Windowsで作られたZIPが文字化けしたら

ZIPの中のファイル名が、見たこともない記号の列になっている。日本語の環境では、これがかなりの頻度で起こります。

原因は、ファイル名の文字コードです。ZIPの規格では、ファイル名は昔ながらのIBMの文字コードか、UTF-8のどちらかだと決まっています。

ところが古いWindowsのツールの中には、日本語のファイル名をShift_JIS系のcp932で書き込むものがあります。zipfileはそれを規格どおりに読もうとして、文字化けしてしまうのです。

Python 3.11からは、metadata_encodingで読み込み時の文字コードを指定できるようになりました。

with zipfile.ZipFile("from_windows.zip", metadata_encoding="cp932") as zf:
    print(zf.namelist())

この指定は読み込みモードのときだけ使えます。また、ZIPの中にUTF-8だという印が付いている場合は、そちらが優先されます。

逆に、zipfileで日本語名のファイルを書き込むと、自動でUTF-8が使われます。文字コードがそもそもよくわからないという方は、こちらで基本を整理できます。【関連記事】Pythonの文字コードとは?文字化けとUnicodeDecodeErrorの直し方を初心者向けに解説

信頼できないZIPは、そのまま展開しない

全部を展開するなら、extractall()を呼ぶだけです。とても便利ですが、ここには落とし穴があります。

公式ドキュメントには、信頼できない相手から受け取ったZIPを、確認せずに展開してはいけないとはっきり書かれています。どんな危険があるのでしょうか。

ZIPには、2種類の危険が潜んでいる

1つめは、ファイル名に../を含めて、展開先の外へファイルを書き込もうとするZIPです。zipfileは..を取り除くなどの対策をしてくれますが、公式ドキュメントの表現も、防ごうと試みる、にとどまっています。

2つめは、ZIP爆弾と呼ばれるものです。ZIPとしては数KBしかないのに、展開すると巨大なサイズに膨れ上がり、ディスクを埋め尽くします。

さらに、同じZIPを2回展開すると、既存のファイルを確認なしに上書きする点も覚えておきましょう。

展開する前に、名前とサイズを確かめる

そこで、展開の前にチェックを挟む関数を作ってみます。ファイル数、展開後の合計サイズ、展開先の外に出ないかの3点を確かめています。

import zipfile
from pathlib import Path

MAX_FILES = 1000
MAX_TOTAL_SIZE = 100 * 1024 * 1024  # 展開後の合計は100MBまで


def safe_extract(zip_path, dest):
    dest = Path(dest).resolve()
    with zipfile.ZipFile(zip_path) as zf:
        infos = zf.infolist()
        if len(infos) > MAX_FILES:
            raise ValueError(f"ファイル数が多すぎます: {len(infos)}")

        total = sum(info.file_size for info in infos)
        if total > MAX_TOTAL_SIZE:
            raise ValueError(f"展開後のサイズが大きすぎます: {total:,} バイト")

        for info in infos:
            target = (dest / info.filename).resolve()
            if not target.is_relative_to(dest):
                raise ValueError(f"危険なパスが含まれています: {info.filename}")

        zf.extractall(dest)
    return [info.filename for info in infos]

試しに、../../etc/evil.txtという名前のファイルを入れたZIPを渡してみました。すると展開する前に止まり、次のメッセージが出ます。

危険なパスが含まれています: ../../etc/evil.txt

is_relative_to()は、パスが指定したフォルダの中にあるかを調べるpathlibのメソッドで、Python 3.9から使えます。パスの扱いに慣れていない方は、こちらもどうぞ。【関連記事】Pythonのpathlibとは?ファイルパス操作を初心者向けに解説

なお、file_sizeはZIPの中に書かれた申告上の値です。完全な防御ではなく、最初の関門と考えてください。

私自身、Webアプリでユーザーがアップロードしたファイルを扱う仕事をしたとき、レビューでいちばん指摘されたのがこの展開処理でした。便利な1行ほど、何を信用しているのかを一度立ち止まって考える価値があります。

コマンドラインからも使える

ちょっと中身を見たいだけなら、Pythonのコードを書く必要すらありません。python -m zipfileで、ZIPの操作ができます。

主なオプションを表にまとめました。

オプション できること 例
-l 中身の一覧を表示 python -m zipfile -l report.zip
-c ZIPを作る python -m zipfile -c out.zip report/
-e 指定フォルダへ展開 python -m zipfile -e report.zip out/
-t ZIPが壊れていないか検査 python -m zipfile -t report.zip

OSを問わず同じコマンドで動くのがうれしいところです。

コードの中で壊れていないかを調べたいときは、testzip()を使います。問題がなければNoneが返り、壊れたファイルがあれば最初の1つの名前が返ります。

zipfileでつまずきやすいポイントのまとめ

ZIPを作るときは、compression=zipfile.ZIP_DEFLATEDを忘れないこと。省略すると、まとめるだけで小さくなりません。

読むときは、いきなり展開せずにnamelist()で中身を確認すること。CSVならopen()とio.TextIOWrapperで、展開せずに読めます。

文字化けしたら、metadata_encoding="cp932"を試すこと。そして信頼できないZIPは、名前とサイズを確かめてから展開することです。

ZIPまわりの作業は、毎月のように発生するわりに、手作業のまま放置されがちです。まずは今週のどれか1つを、zipfileで置き換えてみてください。

ここまでお読みいただきありがとうございました。

参考情報

次のアクション

記事で学んだ内容を実際に動かしてみよう

Python WebAcademyでは、ブラウザ上でコードを書きながら基礎から実践まで体系的に学べます。

Python WebAcademyの学習画面

あわせて読む

関連記事

ブログ一覧へ

Python学習ロードマップ

まずはこの3講座から

記事で気になったテーマを、順番に手を動かしながら学べます。

ロードマップを見る