用 Python 校验 ZIP 备份:检查 CRC、重复成员和漏打包文件
备份脚本返回成功,backup.zip 也存在,却不一定能恢复:复制中断可能留下残缺归档,打包参数写错还可能漏掉配置文件。本文给初级到中级读者一个只读检查工具,先核对 ZIP 成员,再完整读取并检查 CRC,最后用退出码决定是否继续。
1. 方法:成员清单和完整读取都要检查
已有的 zipfile 基础教程介绍了读写和解压。本篇专门为已经写完、校验期间不会变化的本地备份添加验收步骤:拒绝重复成员名,核对必需文件,再调用 testzip()。
Python 官方 testzip 文档说明,该方法读取归档内文件并检查 CRC 和文件头,发现问题时返回首个坏文件名,否则返回 None。只列出目录并不能完成这项检查。
这里仅接受未加密的 ZIP_STORED 和 ZIP_DEFLATED 归档,并拒绝没有文件的归档。CRC 可帮助发现意外损坏;若要与可信原件核对内容,可配合 SHA-256 文件清单。本工具不验证 JSON、CSV 的业务内容,也不保证以后解压时的路径安全。
2. 准备环境和演示备份
使用 Python 3.9 或更新版本及可用的 zlib 模块,无需安装第三方包。示例在 Linux、Python 3.11.4 下测试,命令适用于 bash 和 zsh。
- 创建新演示目录;后续命令在同一个终端和目录运行。重复实验时请换一个目录名:
python3 --version
mkdir zip-backup-demo
cd zip-backup-demo
python3 - <<'PY_DATA'
from zipfile import ZipFile, ZIP_DEFLATED
with ZipFile("backup.zip", "x", compression=ZIP_DEFLATED) as archive:
archive.writestr("config/task.json", '{"enabled":true}\n')
archive.writestr("reports/daily.csv", "id,value\n1,42\n")
PY_DATA
- 将下一节保存为
check_zip.py,或下载 完整检查脚本。示例使用x模式创建 ZIP,已有同名归档时会报错,避免覆盖。
3. 完整实现
"""Check a completed local ZIP backup without extraction. Python 3.9+."""
import argparse
import json
from pathlib import Path
import sys
import zipfile
import zlib
def check_zip(path, required):
with zipfile.ZipFile(path, "r") as archive:
seen = set()
files = set()
total = 0
for info in archive.infolist():
name = info.filename
if name in seen:
raise ValueError(f"duplicate member: {name!r}")
seen.add(name)
if info.flag_bits & 1:
raise ValueError(f"encrypted member is unsupported: {name!r}")
if info.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
raise ValueError(f"unsupported compression: {name!r}")
if not info.is_dir():
files.add(name)
total += info.file_size
if not files:
raise ValueError("archive contains no files")
missing = sorted(set(required) - files)
if missing:
raise ValueError(f"missing required members: {missing!r}")
bad = archive.testzip()
if bad is not None:
raise ValueError(f"CRC/header check failed: {bad!r}")
return {
"file": str(path), "files": len(files),
"uncompressed_bytes": total, "valid": True,
}
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("file", type=Path, help="completed, unchanging ZIP backup")
parser.add_argument(
"--require", action="append", default=[], metavar="MEMBER",
help="exact non-directory member name; repeat for multiple files",
)
args = parser.parse_args()
if any(not name or name.endswith("/") for name in args.require):
parser.error("--require needs a nonempty file member name, without trailing /")
try:
result = check_zip(args.file, args.require)
except (OSError, ValueError, EOFError, RuntimeError,
NotImplementedError, zipfile.BadZipFile, zlib.error) as exc:
print(f"ERROR: {args.file}: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
infolist()返回成员信息;ZIP 可以包含同名成员,因此在完整读取前先拒绝重复名,避免“读取的是哪一份”产生歧义。--require 区分大小写,按归档内的完整名称精确匹配,使用 / 分隔目录;目录条目不能满足文件要求。
程序不调用解压方法,但会解压数据流进行校验,耗时随内容增长;目录信息也会占内存。它面向可信来源的自有备份,没有资源限额或恶意归档隔离机制,不应用作开放上传接口的安全检查器。
4. 运行并核对报告
- 查看帮助,检查两个必需文件:
python3 check_zip.py --help
python3 check_zip.py backup.zip \
--require config/task.json --require reports/daily.csv > report.json
cat report.json
预期输出:
{"file": "backup.zip", "files": 2, "uncompressed_bytes": 31, "valid": true}
uncompressed_bytes 是非目录成员声明的未压缩大小总和,不是 ZIP 文件占用的磁盘空间。报告仅在所有检查通过后输出。
- 检查报告内容,并确认没有解压到当前目录:
python3 - <<'PY_CHECK'
import json
from pathlib import Path
report = json.loads(Path("report.json").read_text(encoding="utf-8"))
assert report == {
"file": "backup.zip", "files": 2,
"uncompressed_bytes": 31, "valid": True,
}
assert not Path("config").exists()
assert not Path("reports").exists()
print("PASS: two members checked; no files extracted")
PY_CHECK
普通失败返回 1,并写 stderr;参数错误返回 2,这是 argparse 的默认行为。成功返回 0。重定向会提前创建或截断 report.json,请使用新的报告路径。
5. 验证截断、CRC 损坏和漏文件
- 只在演示目录构造故障副本;保留原始
backup.zip:
python3 - <<'PY_BAD'
from pathlib import Path
from zipfile import ZipFile, ZIP_STORED
import warnings
Path("truncated.zip").write_bytes(Path("backup.zip").read_bytes()[:20])
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
with ZipFile("duplicate.zip", "x") as archive:
archive.writestr("same.txt", "first")
archive.writestr("same.txt", "second")
with ZipFile("crc-bad.zip", "x", compression=ZIP_STORED) as archive:
archive.writestr("note.txt", b"unique-demo-payload")
raw = Path("crc-bad.zip").read_bytes()
assert raw.count(b"unique-demo-payload") == 1
Path("crc-bad.zip").write_bytes(raw.replace(
b"unique-demo-payload", b"Unique-demo-payload", 1
))
PY_BAD
crc-bad.zip 使用未压缩存储,替换一个等长内容字节,让成员清单仍可读,但 CRC 与实际内容不一致。
- 验证失败退出、空 stdout 和输入未改动:
python3 - <<'PY_TEST'
from pathlib import Path
import subprocess
import sys
cases = [
(["truncated.zip"], "File is not a zip file"),
(["duplicate.zip"], "duplicate member"),
(["crc-bad.zip"], "CRC/header check failed"),
(["backup.zip", "--require", "missing.csv"], "missing required members"),
]
for args, expected in cases:
path = Path(args[0])
before = path.read_bytes()
result = subprocess.run(
[sys.executable, "check_zip.py", *args],
capture_output=True, text=True, check=False,
)
assert result.returncode == 1, result
assert result.stdout == "", result.stdout
assert expected in result.stderr, result.stderr
assert path.read_bytes() == before
print("PASS: four failures rejected; archives unchanged")
PY_TEST
预期输出 PASS: four failures rejected; archives unchanged。这四种失败都不会产生成功 JSON;重复成员和漏文件在读取内容前就会被拒绝。
6. 接入自动化与排错
只在检查成功时继续下一步,下面用一句输出模拟后续任务:
if python3 check_zip.py backup.zip --require config/task.json; then
python3 -c 'print("NEXT: backup accepted")'
else
printf 'STOP: backup validation failed\n' >&2
exit 1
fi
将输入改为 crc-bad.zip 后,检查失败并阻止后续输出。真实任务应使用同一份未变化的备份;发布或复制完成后再检查,归档仍在写入时不要启动验收。
| 现象 | 具体处理 |
|---|---|
File is not a zip file |
确认下载的不是 HTML 错误页;重新复制或生成完整备份。 |
CRC/header check failed |
保留坏文件作排查,从可信源重新生成或传输,再跑检查。 |
duplicate member |
检查是否反复使用追加模式;在新路径重新打包,每个成员只写一次。 |
missing required members |
核对归档内路径和大小写,例如 config/task.json,不要填服务器绝对路径。 |
encrypted member / unsupported compression |
按本工具约定生成未加密的 STORED/DEFLATED 备份,或使用适配原格式的校验工具。 |
archive contains no files |
检查输入目录和打包过滤规则,目录条目本身不算文件。 |
如果备份只是单个 .gz 日志,应使用 gzip 截断与 CRC 检查教程,不要把 gzip 文件交给 ZIP 工具。
总结
先核对必需成员和重复名称,再完整读取 ZIP 检查 CRC,通过后才输出成功报告。用故障副本验证失败退出,让自动化流程确实停在检查步骤。重要备份还应结合可信哈希、内容校验和恢复演练。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-zip-backup-check.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。