Pythonで作業フォルダを自動バックアップ!zipで日付付き保存・除外設定・古い世代の削除まで
「納品前のファイルを上書きしてしまった…」「昨日の状態に戻したいのに、手元に残っていない…」
フリーランスや副業で案件を進めていると、こんなヒヤッとする場面、ありますよね。クラウドの同期フォルダに入れていても、上書きや削除まで同期されてしまうことがあります。
この記事では、Pythonの標準ライブラリだけで、作業フォルダを日付付きのzipにバックアップするスクリプトを作ります。.venv や node_modules のような重いフォルダを除外し、古いバックアップを自動で消す「世代管理」、最後にzipが壊れていないかの確認まで一緒に見ていきましょう!
対象は「for文と関数は書ける」くらいの初〜中級者の方です。追加のインストールは不要です😊
まずは1行でzipにする:shutil.make_archive

いちばん手軽なのが shutil.make_archive() です。イメージとしては「フォルダを右クリックして圧縮」をPythonから行う感じです。
ここでは、案件フォルダ client_a_site を backups フォルダにzipで保存します。ファイル名に日時を入れておくと、いつのバックアップか一目でわかります。
import shutil
from datetime import datetime
from pathlib import Path
SRC = Path("client_a_site") # バックアップしたいフォルダ
DEST = Path("backups") # zipの保存先
DEST.mkdir(exist_ok=True)
# 例: client_a_site_20261023_090000
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
base_name = DEST / f"{SRC.name}_{stamp}"
# 拡張子 .zip は自動で付く
zip_path = shutil.make_archive(str(base_name), "zip", root_dir=SRC)
size_kb = Path(zip_path).stat().st_size / 1024
print(f"作成: {Path(zip_path).name}({size_kb:.1f} KB)")
ポイントをまとめるとこんな感じです👇
- 第1引数はファイル名から拡張子を除いたもの(
.zipは自動で付きます) root_dirに指定したフォルダの中身がzipに入ります- 戻り値は作成したzipのパスです
実行すると、作成: client_a_site_20261023_090115.zip(130.5 KB) と表示されました。
動きました!…が、130.5 KBもあります。中身はソースコードとメモ数個だけなのに、なぜでしょう?
答えは、.venv(仮想環境)や node_modules、__pycache__ まで丸ごと入っているからです。これらはあとから作り直せるファイルなので、バックアップに入れても容量を食うだけなんですよね。実際の案件では数百MBになることも珍しくありません⚠️
除外設定と世代管理つきのバックアップを作る
そこで、zipfile モジュールで1ファイルずつ追加しながら、不要なものを飛ばすようにします。あわせて、最新の3世代だけ残して古いものは削除する処理も入れます。
ここが重要です👇
EXCLUDE_DIRS:途中にこのフォルダ名があれば、その下は全部スキップEXCLUDE_FILES:fnmatchで*.tmpのようなワイルドカード指定- ファイル名が
名前_年月日_時分秒.zipなので、名前順に並べると古い順になる
import zipfile
from datetime import datetime
from fnmatch import fnmatch
from pathlib import Path
SRC = Path("client_a_site")
DEST = Path("backups")
KEEP = 3 # 残す世代数
# 入れないフォルダ名・ファイル名のパターン
EXCLUDE_DIRS = {".venv", "node_modules", "__pycache__", ".git"}
EXCLUDE_FILES = ["*.tmp", "*.pyc", ".DS_Store"]
def is_excluded(path: Path) -> bool:
rel = path.relative_to(SRC)
# 途中のフォルダに除外対象が1つでもあればスキップ
if any(part in EXCLUDE_DIRS for part in rel.parts[:-1]):
return True
return any(fnmatch(path.name, pat) for pat in EXCLUDE_FILES)
def make_backup() -> Path:
DEST.mkdir(exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
zip_path = DEST / f"{SRC.name}_{stamp}.zip"
added, skipped = 0, 0
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for path in sorted(SRC.rglob("*")):
if not path.is_file():
continue
if is_excluded(path):
skipped += 1
continue
# zipの中は「client_a_site/src/app.py」のような相対パスにする
zf.write(path, arcname=path.relative_to(SRC.parent))
added += 1
size_kb = zip_path.stat().st_size / 1024
print(f"作成: {zip_path.name}({added}ファイル / 除外 {skipped} / {size_kb:.1f} KB)")
return zip_path
def remove_old_backups() -> None:
# 名前に日時が入っているので、名前順=古い順
backups = sorted(DEST.glob(f"{SRC.name}_*.zip"))
for old in backups[:-KEEP]:
old.unlink()
print(f"削除: {old.name}")
print(f"残っているバックアップ: {min(len(backups), KEEP)}個")
if __name__ == "__main__":
make_backup()
remove_old_backups()

このスクリプトを4回続けて実行してみました。上の画像が4回目の結果です。
130.5 KB → 0.5 KBに減りました🚀 除外された4つは、.venv・node_modules・__pycache__ の中のファイルと cache.tmp です。
そして4つ目ができたタイミングで、いちばん古い1つ目(090115)が削除され、3世代が残っています。これなら、毎日動かしてもディスクがいっぱいになりません。
ちなみに arcname=path.relative_to(SRC.parent) としているので、zipの中は client_a_site/src/app.py のようにフォルダ名から始まる形になります。展開したときにファイルが散らばらないので、地味に便利です。
⚠️ 世代削除の注意点
globのパターンはclient_a_site_*.zipのように案件名まで入れて絞ること。*.zipにすると、別案件のzipまで消してしまいます- 同じ秒に2回実行すると同じファイル名になり上書きされます。手動で連打しない運用なら問題ありません
バックアップが本当に使えるか確かめる
バックアップは「いざ戻すときに使える」ことが一番大事です。作りっぱなしにせず、testzip() で破損チェックをして、別フォルダに展開してみましょう。
import zipfile
from pathlib import Path
DEST = Path("backups")
# いちばん新しいバックアップを調べる
latest = sorted(DEST.glob("client_a_site_*.zip"))[-1]
with zipfile.ZipFile(latest) as zf:
bad = zf.testzip() # 壊れたファイルがあればその名前、なければ None
print(f"確認: {latest.name}")
print("破損チェック:", "OK" if bad is None else f"NG({bad})")
for info in zf.infolist():
print(f" {info.filename} {info.file_size}バイト")
# 別フォルダに展開して中身を確かめる
zf.extractall("restore_test")
print("restore_test フォルダに展開しました")

実行結果は上の画像のとおりです。「破損チェック: OK」となり、3つのファイルが展開できました。
testzip()は中のファイルを全部読み、CRC(チェック用の値)が合わないファイルがあればその名前を、問題なければNoneを返します- 一覧を見ると、除外したフォルダが入っていないことも確認できます
- 展開先は元のフォルダとは別の場所にしましょう。元ファイルを上書きしてしまうと本末転倒です
毎日自動で動かすには
スクリプトができたら、Windowsならタスクスケジューラ、Mac・Linuxなら cron に登録すれば、毎日決まった時刻にバックアップが取れます。そのときは SRC と DEST を絶対パスにしておくのがコツです。自動実行では「今いるフォルダ」がスクリプトの場所とは限らないためです。
また、保存先は作業フォルダと別のドライブや外付けSSDにしておくと、ディスク故障にも備えられます。
より詳しい仕様は公式ドキュメントも参考にしてください。
まとめ
shutil.make_archive()なら1行でフォルダをzipにできる。ただし.venvなども全部入るzipfile+rglobで1ファイルずつ追加すれば、除外設定ができる(今回は 130.5 KB → 0.5 KB)- ファイル名に日時を入れれば、名前順=古い順。新しい3世代だけ残すのも数行で書ける
testzip()と展開テストで、「戻せるバックアップ」になっているか確認する
「消えてから後悔」を防ぐ仕組みは、作っておくだけで気持ちがぐっとラクになります。ご自身の作業フォルダに合わせて除外リストを調整して、ぜひ試してみてください!
こちらも読まれています
📚 関連商品・おすすめ書籍
※本記事にはアフィリエイトリンクが含まれます。




