登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

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 时返回“结构损坏”。日志里保留文件大小、上传请求编号和上游响应类型,通常比只记录扩展名更容易找到真正原因。

Python is_zipfile 快速验证后进入 ZipFile 上下文并区分文件头失败与 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,最后才打开源成员和目标文件。不要先写文件再补做检查。

Python ZipFile 成员 filename 经过绝对路径与父目录检查后写入 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
  • 逐项拒绝绝对路径、父目录片段和超出解压资源上限的成员。
  • 在临时目录完成必需文件核对,成功后才切换到正式目录。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>