フォルダー内のファイル一覧をCSVで作成する
サブフォルダーを走査し、ファイルパス、拡張子、サイズ、更新日時を表として記録します。元のファイルと既存の結果をそのまま保持したまま、まず4つの小さなサンプルファイルで確認します。
元のファイルを変更せずにフォルダから日付入り ZIP アーカイブを作成し、すべての元ファイルが内容一致の状態でアーカイブに含まれていることを検証します。小さな合成フォルダを使うため、想定されるパス、サイズ、合計値を手作業でも確認できます。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者アーカイブ作成が成功したと仮定するのではなく、明示的な検証手順を含む簡単な日付入りフォルダバックアップを作成したい人向けのガイドです。
スクリプトは、1つの元フォルダ内にある通常ファイルを、ネストされたサブフォルダ内のファイルも含めてアーカイブします。ZIP 内のパスは元フォルダからの相対パスとして保存されるため、reports/week1.txt のようなファイルは絶対ファイルシステムパスではなく、そのまま reports/week1.txt として保存されます。
アーカイブ名には現在のローカル暦日を YYYY-MM-DD 形式で含めます。この例を 2026-09-20 に実行した場合、想定されるファイル名は backup_demo_2026-09-20.zip です。同じ日に同じ保存先で再実行すると、以前のバックアップを置き換えるのではなくスクリプトが停止します。
次のデータセットは合成データであり、この記事のために作成したものです。セットアップスクリプトを create_backup_demo.py として保存してください。ネストされた reports フォルダ内の1ファイルを含む、正確なバイト内容を持つ4つのファイルを作成します。
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)
python create_backup_demo.py| 相対パス | 正確な内容 | バイト数 |
|---|---|---|
| notes.txt | alpha と改行 | 6 |
| reports/week1.txt | beta と改行 | 5 |
| reports/week2.txt | gamma と改行 | 6 |
| settings.ini | mode=test と改行 | 10 |
元フォルダには4つの通常ファイルがあり、非圧縮内容の合計は 27 bytes です: 6 + 5 + 6 + 10。最終的な ZIP ファイル自体のサイズは、ZIP 圧縮とアーカイブメタデータに独自のオーバーヘッドが加わるため、この合計値からは予測できません。
python dated_zip_backup.pyスクリプトは元フォルダが存在することと、バックアップディレクトリが元フォルダの内部にないことを確認します。バックアップの親フォルダはすでに存在していても構いません。日付入り ZIP ファイル自体は存在していてはいけません。
スクリプトはまず、元ファイルのソート済み一覧を作成します。排他的ファイル作成でアーカイブを作成し、各ファイルを ZIP_DEFLATED 圧縮で書き込んだ後、検証のためにアーカイブを再度開きます。検証では、意図したメンバー一覧を比較し、testzip で CRC 整合性を確認し、元ファイルとアーカイブ済みファイルの SHA-256 ハッシュを比較します。
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()
コードはシンボリックリンクをスキップし、通常ファイルだけをアーカイブします。想定ハッシュは、各ファイルを追加する直前に元ファイルから計算されます。実行中に変更されない通常のローカルフォルダであれば、アーカイブ作成後の有用な内容レベル比較になります。
スクリプトを 2026-09-20 に実行した場合、想定されるアーカイブパスは outputs/backups/backup_demo_2026-09-20.zip です。ZIP には、次の4つのメンバーパスだけが含まれるはずです。
| アーカイブメンバー | 非圧縮バイト数 |
|---|---|
| notes.txt | 6 |
| reports/week1.txt | 5 |
| reports/week2.txt | 6 |
| settings.ini | 10 |
ネストされた reports フォルダは、メンバーパス自体によって表現されます。ファイルがすでにその相対パスを持っているため、スクリプトが reports 用の別個のディレクトリエントリを作成する必要はありません。
以下の想定コンソール出力は、2026-09-20 に実行した場合を手作業で導出したものです。実際に取得した実行ログではありません。
Verified 4 files totaling 27 bytes.
Archive: outputs/backups/backup_demo_2026-09-20.zipアーカイブファイルのサイズを主要な検証基準として使用しないでください。圧縮の影響により、有効なアーカイブでも、データやメタデータによって非圧縮元データの合計より小さくも大きくもなる可能性があります。
| 症状 | 確認すること |
|---|---|
| FileNotFoundError | SOURCE とターミナルの作業ディレクトリを確認します。この例では、先に合成セットアップスクリプトを実行してください。 |
| FileExistsError | 同じ日付入りファイル名のバックアップがすでに存在します。上書きせず、既存内容を確認して別の保存先を選ぶか、意図した別の日付まで待ちます。 |
| PermissionError | スクリプトがすべての元ファイルを読み取れ、BACKUP_DIR 内にファイルを作成できることを確認します。 |
| CRC check failed | アーカイブを失敗として扱います。バックアップとして依存せず、ストレージまたは書き込みエラーを調査して新しいアーカイブを作成します。 |
| Member list mismatch | 実行中に元フォルダが変更されたか、アーカイブ内容が開始時に作成したファイル一覧と一致していない可能性があります。 |
| Hash or size mismatch | 検証失敗として扱います。元データは変更せず保持し、不一致の原因を調査してから新しいアーカイブを作成します。 |
ZIP ファイル作成後に例外が発生した場合、部分的または未検証のアーカイブがバックアップフォルダに残ることがあります。スクリプトは自動削除しません。自動クリーンアップにより、失敗原因の診断に役立つ証拠が失われる可能性があるためです。
これはファイルコピー型のアーカイブワークフローであり、トランザクション型ファイルシステムスナップショットではありません。別のアプリケーションがハッシュ計算中またはアーカイブ中にファイルを変更すると、結果が不整合になる可能性があります。重要なデータでは、元データへの書き込みを停止するか、スナップショット対応のバックアップシステムを使用してください。
この例では、すべてのファイルシステム属性を保持するわけではありません。権限、所有者、アクセス制御リスト、拡張属性、代替データストリーム、シンボリックリンク、アプリケーション固有のメタデータには、別のバックアップツールが必要になる場合があります。ZIP は持ち運び可能なファイル集合には有用ですが、ファイルシステムの完全なイメージではありません。
検証済みアーカイブも依然として1つのコピーにすぎません。実際のバックアップ計画では、複数コピー、ストレージ障害、誤削除、ランサムウェア、保持期間、暗号化、そして復旧が実際にテストされているかを考慮する必要があります。ここでの検証が示すのは、この実行中に観測された元内容とアーカイブが一致したことだけであり、完全な災害復旧戦略が確立されたことを意味しません。
2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: datetime, hashlib, pathlib, zipfile · 未実行
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。