ファイルをまとめて圧縮したい時はZIP、ログやテキストを軽くしたい時はGzipが便利です。
本記事では、Python標準のzipfile
とgzip
を使って、圧縮・解凍を実践的に解説します。
追加インストール不要で今すぐ使える方法に絞り、初心者の方でも迷わないようにサンプルコードと注意点を丁寧に紹介します。
Pythonの圧縮・解凍の基本と選び方(zipfile/gzip)
標準ライブラリで追加インストール不要
Pythonにはzipfile
とgzip
が標準添付されており、どちらもインストールなしで使えます。
学習コストが低く、配布先にも余計な依存を増やさないのが大きな利点です。
zipfileとgzipの違いと用途
両者の違いを整理しておくと選び方が明確になります。
観点 | zipfile | gzip |
---|---|---|
主な形式 | ZIPアーカイブ | Gzip圧縮(単一ストリーム) |
まとめ方 | 複数ファイルやフォルダを1つのZIPに格納可能 | 基本的に1ファイル→1.gz |
代表的な拡張子 | .zip | .gz |
圧縮方式 | DEFLATE(標準)、一部BZIP2やLZMAも可 | DEFLATE |
典型用途 | 配布用アーカイブ、成果物の打包 | テキストやログの軽量化、ストリーム圧縮 |
パス(階層) | ディレクトリ構造を保持 | 構造は持たない |
フォルダごと固めたい→zipfile、単一ファイルをスリム化したい→gzipと覚えると判断が早くなります。
zipfileを使う場面
- プロジェクト一式や成果物をひとまとめに配布したい時に適しています。
- 複数ファイルのディレクトリ構造を保ったまま、1つのファイルにまとめたい時に使います。
gzipを使う場面
- ログやCSV、JSONなど1つのテキストファイルを軽くしたい時に有効です。
- ストリーム処理(標準入出力やネットワーク転送)と相性が良く、パイプラインの途中で圧縮・展開したい時に便利です。
基本用語(圧縮・展開・アーカイブ)
- 圧縮: データサイズを小さくすることです。
- 展開(解凍): 圧縮したデータを元に戻すことです。
- アーカイブ: 複数ファイルやフォルダを1つのまとまりにする形式です。ZIPは圧縮+アーカイブ、Gzipは圧縮のみという違いがあります。
zipfileの使い方(複数ファイルやフォルダをZIP圧縮)
ファイルをZIPに圧縮する
最小の例です。
1つのファイルをZIPに格納します。
# zipfile_basic_file.py
import zipfile
# 圧縮対象のファイルと、作成するZIPファイル名を指定します
src_file = "example.txt"
zip_name = "single_file.zip"
# with構文でクローズを自動化します
# compressionにZIP_DEFLATEDを指定すると一般的な圧縮が有効になります
with zipfile.ZipFile(zip_name, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
# arcnameを指定すると、ZIP内の名前を任意に変えられます
zf.write(src_file, arcname="docs/example.txt")
print(f"Created: {zip_name}")
Created: single_file.zip
フォルダをまるごとZIPにする
フォルダ以下を再帰的に辿って、相対パスを保ちつつZIP化します。
# zipfile_zip_directory.py
import os
import zipfile
def zip_directory(src_dir: str, zip_name: str, compresslevel: int = 6) -> None:
# src_dirの末尾スラッシュの有無に左右されないようにabs化
src_dir_abs = os.path.abspath(src_dir)
with zipfile.ZipFile(zip_name, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=compresslevel) as zf:
for root, _, files in os.walk(src_dir_abs):
for file in files:
full_path = os.path.join(root, file)
# ZIP内に入れるときはsrc_dirからの相対パスを使うのが安全です
arcname = os.path.relpath(full_path, start=src_dir_abs)
zf.write(full_path, arcname=arcname)
# 内容確認(学習用): 実運用では省略可
print("Archive members:")
for name in zf.namelist():
print(" -", name)
if __name__ == "__main__":
zip_directory("project_folder", "project_archive.zip", compresslevel=6)
Archive members:
- README.md
- src/main.py
- data/input.csv
ZIPを解凍する
ZIPを安全に解凍します。
信頼できないZIPを安易に展開するのは危険です。
パストラバーサル(../を含む悪意あるパス)対策を入れた安全な関数例を示します。
# zipfile_extract_safe.py
import os
import zipfile
def safe_extractall(zip_path: str, dest_dir: str) -> None:
# 出力先の絶対パス
dest_abs = os.path.abspath(dest_dir)
with zipfile.ZipFile(zip_path, "r") as zf:
# 各メンバーの展開先パスを検証
for info in zf.infolist():
extracted_abs = os.path.abspath(os.path.join(dest_abs, info.filename))
# commonpathで出力先ディレクトリ配下かを確認
if os.path.commonpath([dest_abs, extracted_abs]) != dest_abs:
raise ValueError(f"Unsafe path detected: {info.filename}")
# 安全が確認できたらまとめて展開
zf.extractall(dest_abs)
if __name__ == "__main__":
os.makedirs("unpacked", exist_ok=True)
safe_extractall("project_archive.zip", "unpacked")
print("Extracted to 'unpacked'")
Extracted to 'unpacked'
圧縮レベルとパスの扱い
圧縮レベルは0〜9を指定できます。
0は無圧縮(保存のみ)、9は最も小さくなるが遅い傾向です。
実務では6前後のバランスが使いやすいことが多いです。
指定例 | 意味 |
---|---|
compresslevel=0 | 無圧縮(ZIP_STORED相当に近い使い方) |
compresslevel=6 | バランス重視の圧縮 |
compresslevel=9 | 最高圧縮(時間とCPUを余計に使う) |
パスについては、arcnameに相対パスを使うことが重要です。
絶対パスやドライブレターをZIP内に入れると、展開時に意図せず上書き事故を招きます。
# zipfile_paths_and_levels.py
import zipfile
with zipfile.ZipFile("levels.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
# ZIP内は相対パスで揃える
zf.write("large.txt", arcname="docs/large.txt")
日本語ファイル名の注意
近年の環境ではUTF-8のファイル名を正しく扱えるツールが増えましたが、非常に古い解凍ソフトでは文字化けすることがあります。
対策として次の方法が現実的です。
- 配布先の解凍ツールを指定し、UTF-8対応を前提にする。
- ZIP内のファイル名はASCIIに近い安全な文字にするため、
arcname
で置き換える。
# 日本語ファイル名を安全な英字名に差し替えて格納する例
import zipfile
with zipfile.ZipFile("safe_name.zip", "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.write("レポート2025年3月版.txt", arcname="report_2025_03.txt")
with構文で安全に扱う
ファイルハンドルの閉じ忘れは不具合の温床です。
with
を使うとクローズが自動化され、途中で例外が発生しても安全です。
# with構文の基本形
import zipfile
with zipfile.ZipFile("data.zip", "w", zipfile.ZIP_DEFLATED) as zf:
zf.write("data.csv", arcname="data/data.csv")
# ここに来た時点で自動的にクローズされています
gzipの使い方(単一ファイルを.gzで圧縮・解凍)
テキストをgzip圧縮する
テキストはgzip.open(..., mode="wt")
で直接書けます。
encodingを明示すると後で扱いやすくなります。
# gzip_text_write.py
import gzip
text = "こんにちは、Gzip圧縮の世界!\n圧縮するとサイズを節約できます。"
with gzip.open("hello.txt.gz", mode="wt", encoding="utf-8", compresslevel=9) as f:
f.write(text)
print("Wrote hello.txt.gz")
Wrote hello.txt.gz
バイナリをgzip圧縮する
バイナリファイル(例: 画像)はrb
で読み、wb
で書きます。
メモリ上のbytes
を一発で圧縮するgzip.compress
も便利です。
# gzip_binary_file.py
import gzip
# ファイルから読み込んで.gzに書く(ストリーム)
with open("image.png", "rb") as src, gzip.open("image.png.gz", "wb", compresslevel=6) as dst:
# 大きいファイルでもそのままコピー可能
dst.write(src.read())
# メモリ上のbytesを圧縮する方法
data = b"A" * 1024 # 1KBのダミーデータ
compressed = gzip.compress(data, compresslevel=6)
decompressed = gzip.decompress(compressed)
assert decompressed == data
print("Binary gzip done")
Binary gzip done
gzipを解凍して読む
rt
でテキストとして読み、rb
でバイナリとして読みます。
# gzip_read.py
import gzip
# テキストとして読む
with gzip.open("hello.txt.gz", "rt", encoding="utf-8") as f:
content = f.read()
print(content.splitlines()[0]) # 先頭行を確認
# バイナリとして読む
with gzip.open("image.png.gz", "rb") as f:
data = f.read()
print(f"Read {len(data)} bytes")
こんにちは、Gzip圧縮の世界!
Read 12345 bytes
圧縮レベルとモード指定
gzip.open
の主なモードは"rb"
、"wb"
、"rt"
、"wt"
です。
圧縮レベルは0〜9で、9が最もサイズが小さくなりやすいですが遅くなります。
再現性が重要ならmtime=0
を指定して、タイムスタンプ差によるバイト列の揺らぎを抑えます。
# gzip_modes_and_levels.py
import gzip
# テキストを書き出す。再現性のためmtime=0を指定
with gzip.open("log.txt.gz", "wt", encoding="utf-8", compresslevel=6, mtime=0) as f:
f.write("line1\nline2\n")
拡張子(.gz)の付け方
一般的には元の拡張子の後ろに.gzを付けます(例: log.txt → log.txt.gz)。
展開後の元ファイル名を保持したい場合はGzipヘッダのfilename
が使われることもありますが、通常はファイル名で管理すれば十分です。
# gzip_extension.py
import os
import gzip
src = "app.log"
dst = src + ".gz" # "app.log.gz"
with open(src, "rb") as fin, gzip.open(dst, "wb") as fout:
fout.write(fin.read())
print(f"Compressed: {dst}")
Compressed: app.log.gz
実用レシピとよくあるつまずき(Python初心者向け)
ディレクトリをZIPにして配布する
配布でありがちな要件を満たす、隠しファイル除外・空ディレクトリ除外の例です。
# recipe_zip_for_distribution.py
import os
import zipfile
def zip_for_distribution(src_dir: str, zip_name: str) -> None:
src_abs = os.path.abspath(src_dir)
with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
for root, dirs, files in os.walk(src_abs):
# .gitなどの隠しディレクトリを除外(必要に応じて拡張)
dirs[:] = [d for d in dirs if not d.startswith(".")]
for file in files:
if file.startswith("."):
continue
full_path = os.path.join(root, file)
arcname = os.path.relpath(full_path, src_abs)
zf.write(full_path, arcname=arcname)
print(f"Packaged: {zip_name}")
if __name__ == "__main__":
zip_for_distribution("my_project", "my_project.zip")
Packaged: my_project.zip
ログを日次でgzip圧縮する
ローテーション済みのログ(例: app-2025-09-16.log)をGzip化する簡単なスクリプトです。
# recipe_gzip_daily_logs.py
import os
import gzip
from datetime import datetime
def gzip_today_log(prefix: str = "app"):
today = datetime.now().strftime("%Y-%m-%d")
src = f"{prefix}-{today}.log"
dst = src + ".gz"
if not os.path.exists(src):
print(f"No log for today: {src}")
return
with open(src, "rb") as fin, gzip.open(dst, "wb", compresslevel=6, mtime=0) as fout:
# mtime=0でビルドの再現性を高める
fout.write(fin.read())
print(f"Compressed: {dst}")
if __name__ == "__main__":
gzip_today_log()
Compressed: app-2025-09-16.log.gz
特定の拡張子だけZIPに含める
ソース配布では.pyや.mdだけ含めたいなど、拡張子で絞ることがよくあります。
# recipe_zip_selected_ext.py
import os
import zipfile
def zip_selected_extensions(src_dir: str, zip_name: str, exts: tuple[str, ...]) -> None:
src_abs = os.path.abspath(src_dir)
with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
for root, _, files in os.walk(src_abs):
for file in files:
if file.lower().endswith(exts):
full_path = os.path.join(root, file)
arcname = os.path.relpath(full_path, src_abs)
zf.write(full_path, arcname=arcname)
print(f"Created: {zip_name}")
if __name__ == "__main__":
# .py と .md を含める
zip_selected_extensions("my_project", "src_docs.zip", (".py", ".md"))
Created: src_docs.zip
エラー対処(ファイル未存在・権限)
初心者の方がつまずきやすいのがFileNotFoundErrorとPermissionErrorです。
最小限のガードを入れましょう。
# recipe_error_handling.py
import os
import zipfile
def safe_zip_write(zip_name: str, file_to_add: str) -> None:
if not os.path.exists(file_to_add):
print(f"Not found: {file_to_add}")
return
try:
with zipfile.ZipFile(zip_name, "a", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
zf.write(file_to_add, arcname=os.path.basename(file_to_add))
print(f"Added to {zip_name}: {file_to_add}")
except PermissionError:
print("Permission denied. Try running with proper permissions or change destination.")
except OSError as e:
print(f"OS error: {e}")
if __name__ == "__main__":
safe_zip_write("assets.zip", "logo.png")
Added to assets.zip: logo.png
展開先フォルダの指定とパス区切り
ZIP内のパスは一般にスラッシュ(/)で表現され、zipfile
はOSに合わせて展開時に良きに計らってくれます。
出力先の指定はextractall(path=...)
で行います。
パス結合はos.path.joinを使うとOS差異(Windowsの円記号など)を気にせずに済みます。
# recipe_extract_to_dir.py
import os
import zipfile
zip_path = "my_project.zip"
out_root = "dist"
subdir = "v1.0.0"
dest = os.path.join(out_root, subdir) # dist/v1.0.0 (OSにより区切りが変化)
os.makedirs(dest, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(path=dest)
print(f"Extracted to: {dest}")
Extracted to: dist/v1.0.0
不審なZIPを展開しないでください。
特にパストラバーサルや巨大な展開サイズ(Zip Bomb)には注意が必要です。
上の安全な展開関数のように検査を入れるのが安心です。
まとめ
複数ファイルやフォルダをまとめるならzipfile、単一ファイルを軽くするならgzipが基本の選び方です。
zipfileではarcnameで相対パスを使う・with構文で自動クローズ、gzipではモード(rt/wt/rb/wb)とencoding、compresslevelの指定がコツです。
日本語ファイル名は可能なら英数字に置き換えるか、UTF-8対応の解凍ツールを前提にしましょう。
さらに不審なZIPの展開は危険である点を忘れず、必要に応じてパス検証や権限チェックを取り入れてください。
これらの基本を押さえることで、日々の配布やログ圧縮などの実務を安全かつ効率的に進めることができます。