ZIPファイル

ZIPファイルを扱うには、Python標準ライブラリの zipfile モジュールを使います(Python 3.11までは zipfile.py という一つのファイルからなるモジュールでしたが、Python 3.12以降は zipfile ディレクトリ以下の複数のファイルで構成されたパッケージです)。

ZIPファイル形式で厄介なのがファイル名のエンコーディングです。元々はIBM PCの8ビット文字コード(Code Page 437、CP437)しかサポートされていませんでしたが、MS-DOSやWindows上のZIPでは、日本語ファイル名はCP932(Shift JISに「機種依存文字」を加えたもの)をそのまま入れていました。一方、Unicode化されたMacやLinuxではUTF-8のバイト列をそのまま入れました。CP437とUTF-8の区別をするために、ZIPファイル中の2バイトの汎用フラグのビット11が当てられましたが、macOSの /usr/bin/zip やFinderのZIP圧縮機能は今もこのビットを立てないようです(さらにFinderで圧縮するとファイル名がUTF-8-MAC(ほぼNFD)になるがこれはOSのバージョンによるかも)。そのため、Macで作ったZIPファイルをWindowsの機能で展開するとファイル名が化けることがあるようです。

次は最低限の機能を備えたZIP展開ツールです(DoraTeXさんのunzip.pyを参考にしました)。最低限の文字化け・ディレクトリトラバーサル脆弱性の対策をしました。

#! /usr/bin/env python3

import sys
from zipfile import ZipFile
from pathlib import Path
from getpass import getpass
import unicodedata

def is_safe_path(path):
    cur = Path(".").resolve()
    path = Path(path).resolve()
    return path.is_relative_to(cur)

with ZipFile(sys.argv[1]) as z:
    for zinfo in z.infolist():
        if zinfo.flag_bits & 0x1:
            password = getpass('PASSWORD: ')
            z.setpassword(password.encode('utf-8'))
        if not zinfo.flag_bits & 0x800:
            try:
                name = zinfo.filename.encode('cp437').decode('cp932')
            except UnicodeDecodeError:
                name = "\x00"
            if not name.isprintable():
                name = zinfo.filename.encode('cp437').decode('utf-8', errors="ignore")
            zinfo.filename = name
        zinfo.filename = unicodedata.normalize('NFC', zinfo.filename)
        if is_safe_path(zinfo.filename):
            print("Extracting", zinfo.filename)
            z.extract(zinfo)
        else:
            print("Skipping", zinfo.filename)

このファイルを unzip.py という名前で作成して実行可能にしておけば、./unzip.py アーカイブ名.zip で展開できます。

なお、現在のZIPファイルは、標準のDeflate圧縮以外に、bzip2、LZMA、Zstandard圧縮に対応しています。ZIPファイル仕様書 APPNOTE.TXT - .ZIP File Format Specification によれば、bzip2は2001年、LZMAは2006年、Zstandardは2020年にサポートされたようです。これらの圧縮形式は、zipfileモジュール以外に、zlib / gzipbz2lzmacompression.zstd の各モジュールでも扱えます。bzip2とLZMAはPython 3.3、ZstandardはPython 3.14でサポートされました。


Last modified: