スクリプト・ファイル自動化

日付入りの ZIP バックアップを作成し、アーカイブ内容を検証する

元のファイルを変更せずにフォルダから日付入り ZIP アーカイブを作成し、すべての元ファイルが内容一致の状態でアーカイブに含まれていることを検証します。小さな合成フォルダを使うため、想定されるパス、サイズ、合計値を手作業でも確認できます。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者アーカイブ作成が成功したと仮定するのではなく、明示的な検証手順を含む簡単な日付入りフォルダバックアップを作成したい人向けのガイドです。

準備するもの
  • Python 3.12 と、そのバージョンを起動できるターミナルコマンド。
  • スクリプトが outputs の下にフォルダを作成できる作業フォルダ。
  • 元ファイルの読み取り権限と ZIP アーカイブを保存できる十分なディスク容量。
  • 必要なのは Python 標準ライブラリのみです: datetime, hashlib, pathlib, zipfile.

01バックアップに含める内容を定義する

スクリプトは、1つの元フォルダ内にある通常ファイルを、ネストされたサブフォルダ内のファイルも含めてアーカイブします。ZIP 内のパスは元フォルダからの相対パスとして保存されるため、reports/week1.txt のようなファイルは絶対ファイルシステムパスではなく、そのまま reports/week1.txt として保存されます。

アーカイブ名には現在のローカル暦日を YYYY-MM-DD 形式で含めます。この例を 2026-09-20 に実行した場合、想定されるファイル名は backup_demo_2026-09-20.zip です。同じ日に同じ保存先で再実行すると、以前のバックアップを置き換えるのではなくスクリプトが停止します。

02合成の元フォルダを作成する

次のデータセットは合成データであり、この記事のために作成したものです。セットアップスクリプトを create_backup_demo.py として保存してください。ネストされた reports フォルダ内の1ファイルを含む、正確なバイト内容を持つ4つのファイルを作成します。

python
from pathlib import Path

SOURCE = Path("outputs") / "backup_demo"
FILES = {
    "notes.txt": b"alpha\n",
    "reports/week1.txt": b"beta\n",
    "reports/week2.txt": b"gamma\n",
    "settings.ini": b"mode=test\n",
}

SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir()  # Stop if the synthetic source already exists.

for relative_name, content in FILES.items():
    target = SOURCE / relative_name
    target.parent.mkdir(parents=True, exist_ok=True)
    with target.open("xb") as stream:
        stream.write(content)
text
python create_backup_demo.py
相対パス正確な内容バイト数
notes.txtalpha と改行6
reports/week1.txtbeta と改行5
reports/week2.txtgamma と改行6
settings.inimode=test と改行10

元フォルダには4つの通常ファイルがあり、非圧縮内容の合計は 27 bytes です: 6 + 5 + 6 + 10。最終的な ZIP ファイル自体のサイズは、ZIP 圧縮とアーカイブメタデータに独自のオーバーヘッドが加わるため、この合計値からは予測できません。

03日付入りバックアップ実行を準備する

  1. メインスクリプトを dated_zip_backup.py としてセットアップスクリプトと同じ場所に保存します。
  2. この例では SOURCE を outputs/backup_demo のままにします。
  3. アーカイブが元フォルダの外側に作成されるよう、BACKUP_DIR を outputs/backups のままにします。
  4. 同じ作業ディレクトリからスクリプトを実行します。
text
python dated_zip_backup.py

スクリプトは元フォルダが存在することと、バックアップディレクトリが元フォルダの内部にないことを確認します。バックアップの親フォルダはすでに存在していても構いません。日付入り ZIP ファイル自体は存在していてはいけません。

04ZIP を作成し、すべてのメンバーを検証する

スクリプトはまず、元ファイルのソート済み一覧を作成します。排他的ファイル作成でアーカイブを作成し、各ファイルを ZIP_DEFLATED 圧縮で書き込んだ後、検証のためにアーカイブを再度開きます。検証では、意図したメンバー一覧を比較し、testzip で CRC 整合性を確認し、元ファイルとアーカイブ済みファイルの SHA-256 ハッシュを比較します。

python
from datetime import date
import hashlib
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

SOURCE = Path("outputs") / "backup_demo"
BACKUP_DIR = Path("outputs") / "backups"
CHUNK_SIZE = 1024 * 1024


def hash_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        while chunk := stream.read(CHUNK_SIZE):
            digest.update(chunk)
    return digest.hexdigest()


def hash_member(archive: ZipFile, member_name: str) -> str:
    digest = hashlib.sha256()
    with archive.open(member_name, "r") as stream:
        while chunk := stream.read(CHUNK_SIZE):
            digest.update(chunk)
    return digest.hexdigest()


def main() -> None:
    source_root = SOURCE.resolve(strict=True)
    if not source_root.is_dir():
        raise ValueError("SOURCE must be a directory.")

    backup_root = BACKUP_DIR.resolve()
    if backup_root.is_relative_to(source_root):
        raise ValueError("BACKUP_DIR must be outside SOURCE.")

    files = sorted(
        path for path in source_root.rglob("*")
        if path.is_file() and not path.is_symlink()
    )
    if not files:
        raise ValueError("SOURCE contains no regular files to back up.")

    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    archive_name = f"{source_root.name}_{date.today().isoformat()}.zip"
    archive_path = BACKUP_DIR / archive_name
    if archive_path.exists():
        raise FileExistsError(f"Backup already exists: {archive_path}")

    expected = {}
    with archive_path.open("xb") as raw:
        with ZipFile(raw, "w", compression=ZIP_DEFLATED) as archive:
            for path in files:
                relative = path.relative_to(source_root).as_posix()
                expected[relative] = (path.stat().st_size, hash_file(path))
                archive.write(path, arcname=relative)

    with ZipFile(archive_path, "r") as archive:
        bad_member = archive.testzip()
        if bad_member is not None:
            raise RuntimeError(f"CRC check failed: {bad_member}")

        members = [info for info in archive.infolist() if not info.is_dir()]
        actual_names = [info.filename for info in members]
        expected_names = list(expected)
        if actual_names != expected_names:
            raise RuntimeError("Archive member list does not match the source list.")

        for info in members:
            expected_size, expected_hash = expected[info.filename]
            if info.file_size != expected_size:
                raise RuntimeError(f"Size mismatch: {info.filename}")
            if hash_member(archive, info.filename) != expected_hash:
                raise RuntimeError(f"Hash mismatch: {info.filename}")

    total_bytes = sum(size for size, _ in expected.values())
    print(f"Verified {len(expected)} files totaling {total_bytes} bytes.")
    print(f"Archive: {archive_path.as_posix()}")


if __name__ == "__main__":
    main()

コードはシンボリックリンクをスキップし、通常ファイルだけをアーカイブします。想定ハッシュは、各ファイルを追加する直前に元ファイルから計算されます。実行中に変更されない通常のローカルフォルダであれば、アーカイブ作成後の有用な内容レベル比較になります。

05想定されるアーカイブを手作業で確認する

スクリプトを 2026-09-20 に実行した場合、想定されるアーカイブパスは outputs/backups/backup_demo_2026-09-20.zip です。ZIP には、次の4つのメンバーパスだけが含まれるはずです。

アーカイブメンバー非圧縮バイト数
notes.txt6
reports/week1.txt5
reports/week2.txt6
settings.ini10

ネストされた reports フォルダは、メンバーパス自体によって表現されます。ファイルがすでにその相対パスを持っているため、スクリプトが reports 用の別個のディレクトリエントリを作成する必要はありません。

以下の想定コンソール出力は、2026-09-20 に実行した場合を手作業で導出したものです。実際に取得した実行ログではありません。

text
Verified 4 files totaling 27 bytes.
Archive: outputs/backups/backup_demo_2026-09-20.zip

06いくつかの独立した確認を行う

  1. アーカイブビューアーで ZIP を開き、想定される相対パスを持つファイルが正確に4つあることを確認します。
  2. アーカイブを別の一時保存先に展開し、テキスト内容を4つの元ファイルと比較します。
  3. バックアップ完了後も元ファイルが outputs/backup_demo に残っていることを確認します。
  4. 保存先を変更せず、同じ日にバックアップスクリプトを再実行します。既存アーカイブを置き換えるのではなく FileExistsError で停止するはずです。
  5. 検証が新しい内容に追従することを確認したい場合は、別のテスト用コピーで元ファイルを変更し、異なるアーカイブ名で新しいバックアップを作成します。

アーカイブファイルのサイズを主要な検証基準として使用しないでください。圧縮の影響により、有効なアーカイブでも、データやメタデータによって非圧縮元データの合計より小さくも大きくもなる可能性があります。

07よくあるエラーを確認する

症状確認すること
FileNotFoundErrorSOURCE とターミナルの作業ディレクトリを確認します。この例では、先に合成セットアップスクリプトを実行してください。
FileExistsError同じ日付入りファイル名のバックアップがすでに存在します。上書きせず、既存内容を確認して別の保存先を選ぶか、意図した別の日付まで待ちます。
PermissionErrorスクリプトがすべての元ファイルを読み取れ、BACKUP_DIR 内にファイルを作成できることを確認します。
CRC check failedアーカイブを失敗として扱います。バックアップとして依存せず、ストレージまたは書き込みエラーを調査して新しいアーカイブを作成します。
Member list mismatch実行中に元フォルダが変更されたか、アーカイブ内容が開始時に作成したファイル一覧と一致していない可能性があります。
Hash or size mismatch検証失敗として扱います。元データは変更せず保持し、不一致の原因を調査してから新しいアーカイブを作成します。

ZIP ファイル作成後に例外が発生した場合、部分的または未検証のアーカイブがバックアップフォルダに残ることがあります。スクリプトは自動削除しません。自動クリーンアップにより、失敗原因の診断に役立つ証拠が失われる可能性があるためです。

08このバックアップで保証されないことを理解する

これはファイルコピー型のアーカイブワークフローであり、トランザクション型ファイルシステムスナップショットではありません。別のアプリケーションがハッシュ計算中またはアーカイブ中にファイルを変更すると、結果が不整合になる可能性があります。重要なデータでは、元データへの書き込みを停止するか、スナップショット対応のバックアップシステムを使用してください。

この例では、すべてのファイルシステム属性を保持するわけではありません。権限、所有者、アクセス制御リスト、拡張属性、代替データストリーム、シンボリックリンク、アプリケーション固有のメタデータには、別のバックアップツールが必要になる場合があります。ZIP は持ち運び可能なファイル集合には有用ですが、ファイルシステムの完全なイメージではありません。

検証済みアーカイブも依然として1つのコピーにすぎません。実際のバックアップ計画では、複数コピー、ストレージ障害、誤削除、ランサムウェア、保持期間、暗号化、そして復旧が実際にテストされているかを考慮する必要があります。ここでの検証が示すのは、この実行中に観測された元内容とアーカイブが一致したことだけであり、完全な災害復旧戦略が確立されたことを意味しません。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: datetime, hashlib, pathlib, zipfile · 未実行

  • 4つの合成ファイルのサイズが 6, 5, 6, 10 bytes であることを手作業で確認しました。
  • 非圧縮元データの合計が 27 bytes であることを手作業で計算しました。
  • ネストされた2つの reports パスを含め、想定されるアーカイブメンバーパスを手作業で列挙しました。
  • 元ファイルが読み取られ、ZIP が outputs の下に書き込まれ、同じ日付のアーカイブがすでに存在する場合に実行が停止することをコードで確認しました。
  • CRC テスト、メンバー一覧比較、非圧縮サイズ比較、SHA-256 比較の検証ロジックを確認しました。
  • 2026-09-20 の想定アーカイブファイル名とコンソール出力を手作業で導出しました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、ZIP アーカイブの作成や展開も行っていません。
  • この例では比較ロジック自体が重要であるため、SHA-256 値や CRC 結果は手作業で計算していません。
  • 同時発生する元ファイルの変更、権限エラー、ストレージ破損、中断された書き込み、シンボリックリンク、大規模ディレクトリでの性能はテストしていません。
  • 圧縮後のアーカイブサイズは、圧縮結果と ZIP メタデータに依存するため予測していません。
  • 公式ドキュメントの URL は既知のドキュメント所在地に基づいて記載していますが、ライブでは確認していません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。