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

Python zipfile.Path 怎样读取压缩包内的嵌套资源

来源:17golang原创

时间:2026-11-14 06:10:54 157浏览 收藏

处理 ZIP 包时,最容易卡住的场景不是“怎么解压”,而是只想读取其中一个多级目录里的配置或模板,却不想把整个压缩包落到本地。zipfile.Path 可以把 ZIP 内部表示成一棵可遍历的虚拟路径树:从根节点开始用 / 或 joinpath() 进入目录,再用 read_text()、read_bytes() 或 open() 读取目标文件。

官方地址:https://docs.python.org/3/library/zipfile.html

要点是把“路径拼接”和“路径安全”分开处理:zipfile.Path 负责访问 ZIP 内的虚拟节点,但不会替你校验不可信成员名。可信来源可以直接按层读取;外部上传的 ZIP 则要先检查成员名,再进行读取或解压。

问题现场:嵌套文件不想先解压

假设压缩包里有这样的结构:

project.zip
├── config/
│   └── app/
│       └── settings.json
└── docs/
    └── readme.txt

传统做法是先调用 extractall(),然后用本地路径读取 config/app/settings.json。如果只需要查看一个小文件,这会多出临时目录、清理目录和文件名冲突等问题。zipfile.Path 提供的是另一条路径:ZIP 仍保持打开状态,代码只在压缩包内部定位和读取目标成员。

ZIP 压缩包根目录通过 zipfile.Path 逐层走到 config app settings json 的结构说明图
图1:zipfile.Path 在 ZIP 内逐层定位嵌套资源的静态结构图,不是运行截图。

初步判断:先从根节点观察结构

不要一开始就把目标路径写死。先把 ZipFile 交给 zipfile.Path,再通过 iterdir() 查看根节点的直接子项。这样可以区分“目录名写错”和“目标文件确实不存在”这两类问题。

from pathlib import Path
from zipfile import Path as ZipPath
from zipfile import ZipFile

archive = Path("project.zip")

with ZipFile(archive) as zf:
    root = ZipPath(zf)
    for child in root.iterdir():
        # 这里只观察 ZIP 根目录的直接子项,不会递归展开全部成员。
        kind = "目录" if child.is_dir() else "文件"
        print(f"{kind}: {child.name}")

这里的 root 不是磁盘上的目录,它代表 ZIP 内部的空路径。iterdir() 只列当前节点的子项;遇到目录时可以继续调用 iterdir(),遇到文件时则应进入读取分支。

动手验证:拼接并读取嵌套资源

确认根节点结构后,就可以用 / 操作符写出和目录层级一致的路径。下面的例子只读取 settings.json,不把它解压到工作目录。

import json
from pathlib import Path
from zipfile import Path as ZipPath
from zipfile import ZipFile

with ZipFile(Path("project.zip")) as zf:
    root = ZipPath(zf)
    settings = root / "config" / "app" / "settings.json"

    if not settings.exists():
        # 先判断成员是否存在,把路径拼写错误和读取异常区分开。
        raise FileNotFoundError(settings.at)
    if not settings.is_file():
        # 目标若是目录,不能直接按 JSON 文件读取。
        raise IsADirectoryError(settings.at)

    text = settings.read_text(encoding="utf-8")
    config = json.loads(text)
    print(config.get("environment", "unknown"))

joinpath("config", "app", "settings.json") 也可以表达同样的路径。较新的 Python 版本还支持 Path.open() 的文本和二进制模式;要读取二进制资源时可以使用 read_bytes(),避免先把内容错误地解码成文本。

from zipfile import Path as ZipPath
from zipfile import ZipFile

with ZipFile("project.zip") as zf:
    root = ZipPath(zf)
    icon = root.joinpath("assets", "icons", "app.bin")
    if icon.is_file():
        # 二进制资源直接保留 bytes,避免编码转换造成数据损坏。
        payload = icon.read_bytes()
        print(f"读取字节数: {len(payload)}")

定位原因:Path 不负责路径安全

如果 ZIP 来自用户上传、第三方下载或消息附件,不能因为使用了 zipfile.Path 就认为路径安全。Python 官方文档明确提示:与 ZipFile.extract() 和 extractall() 会尝试处理成员名不同,Path 不会清理压缩包内的绝对路径或 .. 路径,调用方需要自行校验。

这也是两个容易混淆的边界:

  • zipfile.Path 的职责是访问 ZIP 内的虚拟路径;它不会把文件写到磁盘。
  • extract() 或 extractall() 的职责是写出文件,写出前应防止成员名逃逸目标目录。
zipfile.Path 读取前进行成员名规范化和安全边界判断的流程说明图
图2:读取嵌套资源前的成员名安全边界说明图,不是运行截图。

修复方案:建立安全成员名边界

一个实用的做法是先遍历 ZipInfo 的成员名,只允许相对路径,并拒绝包含空成员、绝对路径或父级跳转的条目。校验通过后,再把确定的相对路径交给 ZipPath 访问。

import posixpath
from pathlib import PurePosixPath
from zipfile import Path as ZipPath
from zipfile import ZipFile


def is_safe_member(name: str) -> bool:
    # ZIP 成员统一按正斜杠处理,拒绝绝对路径和父级跳转。
    normalized = posixpath.normpath(name.replace("\\", "/"))
    parts = PurePosixPath(normalized).parts
    return bool(name) and normalized != "." and not normalized.startswith("/") and ".." not in parts


with ZipFile("project.zip") as zf:
    unsafe = [info.filename for info in zf.infolist() if not is_safe_member(info.filename)]
    if unsafe:
        # 不可信归档中只要出现越界成员,就停止后续读取流程。
        raise ValueError(f"发现不安全的 ZIP 成员: {unsafe[:3]}")

    root = ZipPath(zf)
    target = root.joinpath("config", "app", "settings.json")
    if target.is_file():
        # 通过成员名检查后,再读取目标文件的文本内容。
        settings_text = target.read_text(encoding="utf-8")

这段检查解决的是成员名的路径边界,不等于完整的资源安全策略。生产代码还应按业务限制压缩包总大小、成员数量和单个文件大小,并对编码、重复成员名和损坏归档单独处理。

验证结果:把读取流程固定成几个判断

回到最开始的嵌套资源问题,可以把判断顺序固定下来:先打开 ZipFile,再从 ZipPath(zf) 根节点拼接路径;路径不确定时用 iterdir() 观察;目标节点先判断 exists(),再用 is_file() 或 is_dir() 区分类型;文本、二进制和流式读取分别选择对应方法;外部归档则在读取前检查成员名。

场景推荐写法需要留意
查看根目录ZipPath(zf).iterdir()只返回当前层级子项
访问嵌套文件root / "a" / "b.txt"这是 ZIP 内的虚拟路径
读取文本path.read_text(encoding="utf-8")确认文件编码
读取二进制path.read_bytes()不要误用文本解码
处理外部归档先检查 ZipInfo.filenamePath 不替你清理越界成员

常见问题

zipfile.Path 能直接替代 pathlib.Path 吗?

不能完全替代。两者都有路径拼接和遍历的表达方式,但 zipfile.Path 指向 ZIP 内部成员,不代表本地文件系统;它的读取、写入和安全语义也由 ZIP 容器决定。

为什么 iterdir() 看不到我以为存在的目录?

ZIP 格式允许目录条目缺失,文件名本身也可能带有多级前缀。可以先查看 zf.namelist() 或逐层遍历,再根据实际成员名拼接路径,不要假设每一级都有独立的目录条目。

Path 读取之前一定要解压吗?

不一定。只读文本或二进制成员时,可以保持 ZipFile 打开并直接读取;需要让其他程序使用真实磁盘路径,或需要批量落盘时,才考虑解压流程。

总结

zipfile.Path 适合把“读取 ZIP 内某个嵌套资源”写成清晰的路径操作:从根节点开始遍历,按层拼接,按节点类型判断,再选择文本、字节或打开流。真正容易出问题的地方在安全边界:对于不可信压缩包,路径访问不会自动替你清洗成员名,必须把成员名校验、资源大小限制和异常处理放在读取流程之前。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>