Python zipfile 解压为什么会报 BadZipFile:先验签文件头再处理成员路径
来源:17golang原创
时间:2026-08-28 12:15:59 246浏览 收藏
上传接口收到一个名为 backup.zip 的文件,Python 却在 zipfile.ZipFile 这一行抛出 BadZipFile: File is not a zip file。先别急着把扩展名改成 .zip:这个异常通常说明文件头不是 ZIP、文件还没写完整,或者上游把 HTML 错误页保存成了压缩包名。可靠的处理顺序是先用 is_zipfile 做快速判断,再在 with ZipFile(...) 中打开并检查成员,最后才允许解压到受控目录。
is_zipfile只能做文件头级别的快速筛选,不能替代完整读取和成员校验。BadZipFile要从内容来源、写入是否完成、文件是否被截断三条线排查。- 解压前要拒绝绝对路径和
../穿越路径,目标路径必须落在受控目录内。 - 用临时目录完成解压和清单核对,成功后再原子切换到业务目录。
先确认异常发生在文件头还是完整读取
zipfile.is_zipfile 适合放在上传入口,快速识别文件是否像一个有效 ZIP。它可以接收文件路径,也可以接收可定位的文件对象;返回 False 时不要继续尝试解压。返回 True 也只代表文件头检查通过,后续打开成员仍可能发现损坏。
from pathlib import Path
from zipfile import BadZipFile, ZipFile, is_zipfile
def inspect_zip(path: Path) -> list[str]:
if not is_zipfile(path):
raise ValueError("upload is not a ZIP archive")
try:
with ZipFile(path) as archive:
return archive.namelist()
except BadZipFile as exc:
raise ValueError("ZIP header passed, but archive is damaged") from exc
这里有两个有意分开的状态:文件头不匹配时返回“不是 ZIP”,打开过程中抛出 BadZipFile 时返回“结构损坏”。日志里保留文件大小、上传请求编号和上游响应类型,通常比只记录扩展名更容易找到真正原因。

BadZipFile 的三条排查线不要混在一起
上游返回的可能不是压缩包
下载接口返回 403、登录页或 JSON 错误时,保存代码如果只看 URL 后缀,最后得到的就是一个名叫 backup.zip 的普通文本文件。保存前先核对 HTTP 状态、响应类型和实际字节长度;不要把服务端错误页交给 ZipFile 猜。
写入尚未完成就开始读取
如果一个线程还在写临时文件,另一个线程已经拿它创建 ZipFile,中央目录可能尚未落盘。让生产者先关闭文件,再把完整路径交给消费者;用临时文件名和完成标记区分“正在下载”和“可以读取”。
文件中途被截断
网络断开、磁盘空间不足或上传分片缺失,都可能让文件头还在但中央目录不完整。这类问题通常会在打开或读取成员时暴露,应该保留原始大小与校验摘要,方便和发送端记录比对。
解压前把成员路径收敛到安全目录
压缩包能正常打开,不代表可以无条件调用 extractall。成员名可能包含绝对路径或父目录片段,直接拼接目标目录会把文件写到预期目录之外。下面的函数先规范化成员路径,再确认它仍位于目标目录内;目录项只创建目录,不写入文件。
from pathlib import Path
from zipfile import ZipFile
def extract_to_safe(archive_path: Path, target_dir: Path) -> list[Path]:
target_dir.mkdir(parents=True, exist_ok=True)
extracted: list[Path] = []
with ZipFile(archive_path) as archive:
for member in archive.infolist():
member_path = Path(member.filename)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError(f"unsafe ZIP member: {member.filename}")
destination = (target_dir / member_path).resolve()
if destination != target_dir.resolve() and target_dir.resolve() not in destination.parents:
raise ValueError(f"outside target directory: {member.filename}")
if member.is_dir():
destination.mkdir(parents=True, exist_ok=True)
continue
destination.parent.mkdir(parents=True, exist_ok=True)
with archive.open(member) as source, destination.open("wb") as output:
output.write(source.read())
extracted.append(destination)
return extracted
路径校验和文件写入是同一条数据路径上的两个步骤:先从 member.filename 得到候选位置,再由 destination 判断是否仍在 target_dir,最后才打开源成员和目标文件。不要先写文件再补做检查。

用临时目录完成核对,再提交解压结果
业务代码不应边解压边让其他请求读取目标目录。可以为每次任务创建一个临时目录,完成成员数量、关键文件和大小检查后,再把临时目录切换成正式目录。若任何成员读取失败,清理临时目录,保留错误与归档摘要。
from tempfile import TemporaryDirectory
def unpack_job(archive_path: Path, final_dir: Path) -> int:
with TemporaryDirectory(prefix="zip-check-") as tmp:
staged = Path(tmp)
files = extract_to_safe(archive_path, staged)
if not files or not any(path.name == "manifest.json" for path in files):
raise ValueError("archive has no usable manifest.json")
final_dir.parent.mkdir(parents=True, exist_ok=True)
staged.rename(final_dir)
return len(files)
示例把“解压成功”和“业务可用”分开:前者只说明文件能被写出,后者还要检查必须存在的 manifest.json。生产环境可根据业务增加文件大小上限、总解压大小上限和成员数量上限,避免一个合法格式的压缩包耗尽磁盘。
常见问题:zipfile 解压的几个边界
文件后缀是 zip,为什么 is_zipfile 仍返回 False?
后缀只是文件名,is_zipfile 根据实际内容判断。常见原因是下载到了错误页、文件为空,或上传过程没有完成。
is_zipfile 返回 True,还会抛 BadZipFile 吗?
会。快速检查通过后,完整打开中央目录或读取具体成员仍可能发现截断和结构损坏,所以必须保留 BadZipFile 的异常处理。
能不能直接使用 extractall?
只有在成员路径和资源上限都已经被可信代码检查过时才考虑使用。面向用户上传的压缩包,建议逐项检查成员并写入受控目录。
为什么要先解压到临时目录?
临时目录让校验失败不会污染正式目录,也能避免其他请求看到一半的文件集合。完成清单检查后再切换,回滚边界更清楚。
发布前的最小检查清单
- 记录真实文件大小、来源响应状态和上传任务编号,不用扩展名代替文件类型。
- 先执行
is_zipfile,再用with ZipFile打开并捕获BadZipFile。 - 逐项拒绝绝对路径、父目录片段和超出解压资源上限的成员。
- 在临时目录完成必需文件核对,成功后才切换到正式目录。
-
181 收藏
-
132 收藏
-
225 收藏
-
389 收藏
-
250 收藏
-
文章 · python教程 | 27分钟前 | 日志 · 性能优化 · Python教程 · Python 异步日志 QueueListener logging.QueueHandler queue.Full308 收藏
-
304 收藏
-
353 收藏
-
文章 · python教程 | 5小时前 | 并发 · 异常处理 · python · 异步编程 · Python CancelledError 结构化并发 ExceptionGroup asyncio.TaskGroup414 收藏
-
469 收藏
-
133 收藏
-
文章 · python教程 | 10小时前 | 文件读写 · 数据库 · sqlite3 · Blob · Python教程 · Python 事务 blob sqlite3 blobopen zeroblob366 收藏
-
文章 · python教程 | 11小时前 | 数据校验 · decimal · Python教程 · 金额计算 · Python decimal quantize ROUND_HALF_UP 金额精度196 收藏
-
文章 · python教程 | 12小时前 | 日志 · python · 多进程 · Python QueueListener multiprocessing.Queue logging.QueueHandler474 收藏
-
文章 · python教程 | 13小时前 | 标准库 · python · pathlib · 安全校验 · 文件路径 · Python pathlib 路径校验 Path.relative_to walk_up382 收藏
-
文章 · python教程 | 14小时前 | 调试 · python · inspect · 函数调用 · 参数绑定 · Python 参数校验 关键字参数 inspect.Signature.bind BoundArguments393 收藏
-
文章 · python教程 | 16小时前 | 并发 · 标准库 · 配置管理 · python · 版本升级 · 环境变量 并发安全 os.environ Python 3.14 os.reload_environ428 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习