当前位置: 代码网 > it编程>前端脚本>Python > Python利用zipfile读写压缩包的实战指南

Python利用zipfile读写压缩包的实战指南

2026年09月15日 Python 我要评论
处理用户上传的 zip、批量打包导出文件、解压第三方数据包——zipfile 是绕不开的标准库。但两个坑几乎人人踩过:一个是 read() 一把把整个大文件读进内存,几百 mb

处理用户上传的 zip、批量打包导出文件、解压第三方数据包——zipfile 是绕不开的标准库。但两个坑几乎人人踩过:一个是 read() 一把把整个大文件读进内存,几百 mb 的压缩项直接把服务干 oom;另一个更危险,直接用 extractall 解压不可信的 zip,一个构造好的压缩包能把文件写到 /etc/ 或者你的项目源码目录里,这就是 zip slip 路径穿越漏洞。

这篇把 zipfile 的正确姿势讲清楚:怎么读、怎么写、怎么流式处理不爆内存,以及解压不可信压缩包时必须做的安全防护。

基本读写:先跑通再说

创建一个 zip,把几个文件写进去:

import zipfile

# 'w' 覆盖写;zip_deflated 才会真正压缩,默认 zip_stored 只打包不压缩
with zipfile.zipfile("out.zip", "w", zipfile.zip_deflated) as zf:
    zf.write("report.pdf")               # 按原路径存进去
    zf.write("data/a.csv", "a.csv")      # 第二个参数 arcname:改成压缩包内的路径
    zf.writestr("note.txt", "直接写字符串,不用先落地文件")  # 内存内容直接入包

compression 参数很多人忽略:不传的话默认 zip_stored,只是把文件原样塞进去、体积一点不减。要压缩必须显式指定 zip_deflated(通用)或 zip_lzma(压得更狠但慢)。writestr 尤其好用,把内存里生成的内容(比如导出的 csv 字符串)直接写进包,省去先写临时文件再读的麻烦。

读取和查看内容:

with zipfile.zipfile("out.zip", "r") as zf:
    print(zf.namelist())          # 包内所有条目名列表
    for info in zf.infolist():    # 更详细:每个条目的元信息
        print(info.filename, info.file_size, info.compress_size)

    data = zf.read("note.txt")    # 返回 bytes,整个条目一次读进内存
    print(data.decode("utf-8"))

read() 简单,但注意它把整个条目一次性读进内存——对小文件无所谓,大文件就是隐患,下面细讲。

坑一:read()大文件爆内存,改用流式

假设包里有个 2gb 的日志文件,你要逐行处理:

# ❌ 2gb 条目一次读进内存,直接 oom
with zipfile.zipfile("logs.zip") as zf:
    content = zf.read("huge.log")   # 危险
    for line in content.splitlines():
        handle(line)

正确做法是用 zf.open() 拿到一个文件式对象,像普通文件一样流式读,内存里始终只有一小块:

import io

with zipfile.zipfile("logs.zip") as zf:
    # open 返回二进制流,不会一次性解压全部
    with zf.open("huge.log") as raw:
        # 包裹成 textiowrapper 就能按行迭代,编码自己指定
        for line in io.textiowrapper(raw, encoding="utf-8"):
            handle(line.rstrip("\n"))

zf.open() 返回的是一个边解压边读的流,配合 textiowrapper 就能按行迭代,内存占用和文件大小无关。同理,解压到磁盘时也别用 read() 再自己写,用 shutil.copyfileobj 分块拷贝:

import shutil

with zipfile.zipfile("logs.zip") as zf:
    with zf.open("huge.log") as src, open("huge.log", "wb") as dst:
        # 分块拷贝,内存里只留一个缓冲区
        shutil.copyfileobj(src, dst, length=1024 * 1024)  # 1mb 一块

记住:只要条目可能很大,就用 zf.open() 流式处理,别碰 zf.read()

坑二:zip slip 路径穿越,extractall不可信包等于开后门

这是最危险的一个。你可能觉得解压很安全,一行 extractall 搞定:

# ❌ 对不可信的上传 zip 这样写,等于把写文件的权力交给了攻击者
with zipfile.zipfile(uploaded_zip) as zf:
    zf.extractall("/data/uploads/")

问题在于:zip 内条目的文件名可以是任意字符串,包括 ../../../etc/cron.d/evil 或者绝对路径 /etc/passwd。攻击者构造这样一个包,解压时文件就被写到了 /data/uploads/ 之外——覆盖系统配置、往定时任务目录塞脚本、篡改你的项目代码,都成为可能。这就是 zip slip(cve 遍地都是,java、python、js 生态全中过招)。

先看攻击是怎么造出来的,理解了才好防:

# 演示:构造一个带路径穿越条目的恶意包(仅用于理解攻击)
with zipfile.zipfile("evil.zip", "w") as zf:
    zf.writestr("../../../../tmp/pwned.txt", "escaped!")  # 条目名带 ../

好消息是:python 3.6.2 起,extractall / extract 已经会把带 .. 和绝对路径的条目名做「净化」,把它们重定向到目标目录内,不会真的穿越。但——这个净化只防最经典的 ../,不能全信,而且不同版本行为有差异,符号链接、特殊字符仍可能有边界情况。生产环境处理不可信压缩包,应当自己显式校验每一个解压目标路径:

import os
import zipfile

def safe_extract(zip_path: str, dest_dir: str) -> none:
    dest_dir = os.path.realpath(dest_dir)  # 解析成绝对真实路径,消除符号链接
    with zipfile.zipfile(zip_path) as zf:
        for member in zf.namelist():
            # 拼出目标路径,再算真实绝对路径
            target = os.path.realpath(os.path.join(dest_dir, member))
            # 核心校验:目标必须仍在 dest_dir 之内,否则就是路径穿越
            if not target.startswith(dest_dir + os.sep) and target != dest_dir:
                raise valueerror(f"检测到路径穿越,拒绝解压条目:{member}")
        # 全部校验通过才解压
        zf.extractall(dest_dir)

# safe_extract("evil.zip", "/data/uploads")  # 会抛 valueerror,拒之门外

关键是用 os.path.realpath 把拼出来的目标解析成真实绝对路径(它会展开 .. 和符号链接),再判断结果是否仍以 dest_dir 为前缀。只要跳出了目标目录,一律拒绝。这层校验不依赖标准库版本,自己守住最踏实。

坑三:zip 里的中文文件名乱码

windows 下用资源管理器打的 zip,文件名常用 gbk 编码,而 zip 规范默认 cp437,python 解出来就是一串乱码:

with zipfile.zipfile("windows.zip") as zf:
    for info in zf.infolist():
        name = info.filename
        # 条目没标记 utf-8 时(flag_bits 第 11 位为 0),往往是 cp437 误读的 gbk
        if not info.flag_bits & 0x800:
            # 先按 cp437 编码回 bytes,再用 gbk 正确解码
            name = info.filename.encode("cp437").decode("gbk", errors="replace")
        print(name)

判断依据是 flag_bits & 0x800(通用位标记的第 11 位):置位表示文件名是 utf-8,不用处理;没置位且出现乱码,基本就是 cp437 误读了 gbk,encode("cp437").decode("gbk") 绕回去即可。

追加与防「zip 炸弹」

往已有 zip 追加文件用 'a' 模式:

with zipfile.zipfile("out.zip", "a", zipfile.zip_deflated) as zf:
    zf.writestr("added.txt", "追加进来的内容")

处理不可信包还要防 zip 炸弹——一个几十 kb 的包解压出几十 gb,撑爆磁盘。解压前用 infolist() 检查累计的 file_size,超阈值就拒绝:

max_total = 500 * 1024 * 1024  # 解压上限 500mb
with zipfile.zipfile(untrusted) as zf:
    total = sum(info.file_size for info in zf.infolist())
    if total > max_total:
        raise valueerror(f"解压后体积 {total} 超限,疑似 zip 炸弹")

小结

  • 写包必须显式 compression=zip_deflated,否则默认只打包不压缩、白占空间;writestr 可把内存内容直接入包。
  • 大条目一律用 zf.open() 流式读 + shutil.copyfileobj 分块写,别用 zf.read() 一次吞进内存
  • 解压不可信压缩包必须防 zip slip:用 os.path.realpath 校验每个目标路径仍在目标目录内,跳出就拒绝——别只依赖标准库的内置净化。
  • windows 来源的 zip 中文名乱码,靠 flag_bits & 0x800 判断编码,encode("cp437").decode("gbk") 修复。
  • 不可信包还要防 zip 炸弹,解压前用 infolist() 累加 file_size 设上限。
  • 记忆点:读大用流、解压先验路径、来源不可信就当它有毒——zipfile 的安全底线就这三条。

以上就是python利用zipfile读写压缩包的实战指南的详细内容,更多关于python zipfile读写压缩包的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com