首页
/ CPython zipfile 模块新增 ZipFile.remove 与 ZipFile.repack:原地删除压缩包成员与回收空间的完整指南

CPython zipfile 模块新增 ZipFile.remove 与 ZipFile.repack:原地删除压缩包成员与回收空间的完整指南

2026-09-09 20:10:46作者:柏廷章Berta

本指南以 CPython 源码仓库中 Misc/NEWS.d/next/Library/2025-05-24-11-17-34.gh-issue-51067.yHOgfy.rst 记录的更新条目为骨架,深入讲解标准库 zipfile 新增的 ZipFile.remove()ZipFile.repack() 两个方法。读完本文,你将掌握:如何在不解压、不重建整个压缩包的前提下从 ZIP 归档中删除单个成员;如何调用 repack() 真正回收磁盘空间;以及这两个方法各自的模式约束、参数细节、底层实现原理与对应的官方测试证据,从而在脚本、工具链或应用代码中安全地原地编辑 ZIP 文件。

更新条目背景:gh-issue-51067

本次变更对应 CPython 官方 issue 编号 gh-issue-51067,变更记录位于仓库 Misc/NEWS.d/next/Library/2025-05-24-11-17-34.gh-issue-51067.yHOgfy.rst,全文如下:

Add :meth:~zipfile.ZipFile.remove and `:meth:`~zipfile.ZipFile.repack to :class:~zipfile.ZipFile``.

即:为 zipfile.ZipFile 类新增 remove()repack() 两个实例方法。这是对标准库 zipfile 长期缺失的"原地编辑"能力的补全——此前要从 ZIP 中删除一个成员,唯一的方式是把所有保留的成员重新写入一个新归档,费时费力且需要双倍磁盘空间。

核心实现:remove() 与 repack() 的方法签名与约束

两个新方法都定义在 Lib/zipfile/init.pyZipFile 类中:

def remove(self, zinfo_or_arcname):
    """Remove a member from the archive."""

def repack(self, removed=None, *, strict_descriptor=True,
           chunk_size=_REPACK_CHUNK_SIZE):
    """Repack a zip file, removing non-referenced file entries."""

其中 remove() 位于 Lib/zipfile/init.py#L2374repack() 位于 Lib/zipfile/init.py#L2416_REPACK_CHUNK_SIZE = 2**20(1 MiB)定义于 Lib/zipfile/init.py#L1434

remove(zinfo_or_arcname) 的行为约定

  • 参数:接受一个 ZipInfo 对象,或一个归档内成员名字符串(arcname)。
  • 删除语义:从 ZipFile.filelistZipFile.NameToInfo 两个内部索引中移除该成员;若传入字符串但归档中不存在该名字,会抛出 KeyError(对应测试 Lib/test/test_zipfile/test_core.py#L1544test_remove_by_name_nonexist)。
  • 同名成员处理:ZIP 归档允许存在多个同名条目。当删除其中一个同名成员时,实现会反向遍历 filelist,把仍存在的同名 ZipInfo 回填进 NameToInfo,避免 testzip() 因映射缺失而报错(见 Lib/zipfile/init.py#L2404-L2410)。对应测试 test_remove_by_name_duplicatedLib/test/test_zipfile/test_core.py#L1556)验证了"同名多次删除"场景。
  • 模式约束:仅当 self.mode'w''x''a' 时允许调用,否则抛出 ValueError("remove() requires mode 'w', 'x', or 'a'");归档已关闭(fpNone)或存在正在进行的写入句柄(self._writing 为真)时同样抛出 ValueError。对应测试 test_remove_closedtest_remove_writingtest_remove_mode_r 等位于 Lib/test/test_zipfile/test_core.py#L1710-L1760
  • 返回值:被移除的 ZipInfo 对象。
  • 关键点remove() 只更新中央目录(central directory)与内存索引,不会立即回收归档文件中该成员对应的本地文件头与压缩数据所占的字节——这部分空间要交给 repack() 来回收。

repack(removed=None, *, strict_descriptor=True, chunk_size=...) 的行为约定

  • 模式约束repack() 严格要求 self.mode == 'a'(追加模式),否则抛出 ValueError("repack() requires mode 'a'")。原因在方法文档字符串中说明得很清楚:'w'/'x' 模式关闭时不会对文件做截断,且它们可能作用于不可 seek 的文件缓冲,无法执行截断;而 'a' 模式打开的是可 seek 的真实文件,允许在搬移数据后截断文件尾部。
  • 写入句柄约束:存在进行中的写入(self._writing)或文件引用计数 _fileRefCnt > 1 时抛出 ValueError(见 Lib/zipfile/init.py#L2431-L2434),即压缩包内不能有处于打开状态的读取/写入句柄。
  • 参数
    • removed:可选,一个由先前被 remove() 删除的 ZipInfo 组成的序列。提供时,只剥离这些条目对应的本地文件数据;不提供时,则扫描并剥离所有未被中央目录引用的"孤儿"本地条目(例如通过其他工具直接拼进文件、但从未登记进中央目录的数据)。
    • strict_descriptor:关键字参数,默认 True。决定对使用数据描述符(data descriptor,即 flag_bits_MASK_USE_DATA_DESCRIPTOR 置位)的条目如何定位描述符位置——True 时只认可带 0x08074b50 签名的描述符;False 时额外允许无签名描述符,甚至通过解压追踪压缩块结尾(_scan_data_descriptor_no_sig_by_decompression)来确定边界。
    • chunk_size:关键字参数,默认 _REPACK_CHUNK_SIZE = 2**20(1 MiB),控制搬移数据时单次读写缓冲的大小,见 Lib/zipfile/init.py#L1434
  • 副作用(见 _ZipRepacker.repack 文档字符串,Lib/zipfile/init.py#L1515-L1520):原地修改 ZIP 文件、更新 zfile.start_dir 以反映被移除的数据量、将 zfile._didModify 置为 True,并更新被保留条目的 header_offset、清空其 _end_offset

为什么要分成两个方法:删除索引 vs 回收空间

remove()repack() 的职责是刻意分离的,这是理解整个功能设计的关键:

  1. remove() 操作成本极低:它只改动中央目录与两个内存索引(filelistNameToInfo),随后在 close() 时通过 _write_end_record() 重写结束记录(见 Lib/zipfile/init.py#L2664_didModify 检查)。因此"删一个成员"是即时的、廉价的。
  2. 但被删成员的数据字节仍留在文件中,归档体积不会变小。此时可以:
    • 不调用 repack():归档依然合法可用(中央目录已不引用被删成员),只是文件里残留着不可见的冗余数据;
    • 调用 repack():真正搬移剩余成员的数据、截断文件,把空间还给文件系统。

从测试组织也能看出这一设计:test_remove_* 系列只验证索引与中央目录的正确性;而 test_repack_* 系列则围绕"字节搬移后文件仍有效"展开,例如 test_repack_basicLib/test/test_zipfile/test_core.py#L1801)、test_repack_propagationLib/test/test_zipfile/test_core.py#L1832)等二十余个用例,覆盖了文件前/后/中间残留字节、重复名称、重叠块、无签名数据描述符、坏偏移量等边界场景。

实战用法示例

基本用法:删除一个成员并原地回收空间

import zipfile

# 必须以 'a' 模式打开,repack() 才能工作
with zipfile.ZipFile('data.zip', 'a') as zf:
    zf.remove('obsolete/large.bin')   # 从中央目录移除该成员
    zf.repack()                        # 搬移剩余数据并截断文件,回收磁盘空间

分步执行:先删多个成员,再一次 repack

import zipfile

with zipfile.ZipFile('app.whl', 'a') as zf:
    removed = [
        zf.remove('pkg/__pycache__/mod.cpython-313.pyc'),
        zf.remove('pkg/tests/secret.cfg'),
    ]
    # 传入 removed 序列,只精确剥离这些条目的本地数据,
    # 避免 repack() 对整个文件做全面扫描
    zf.repack(removed)

通过 ZipInfo 删除(适合处理重名成员)

import zipfile

with zipfile.ZipFile('archive.zip', 'a') as zf:
    # infolist() 返回完整条目列表,可精确定位要删除的那一个
    for zi in list(zf.infolist()):
        if zi.is_dir() and zi.filename.endswith('__pycache__/'):
            zf.remove(zi)
    zf.repack()

单独使用 remove()(不回收空间)

import zipfile

# 若只想让归档"逻辑上"不含某成员,可以省略 repack()
with zipfile.ZipFile('data.zip', 'a') as zf:
    zf.remove('tmp/cache.dat')
# close() 时只重写中央目录,文件体积不变

注意:以上所有代码均在打开状态的 ZipFile 对象上执行;若归档已被关闭(fp is None),remove()repack() 都会抛出 ValueError,对应测试见 Lib/test/test_zipfile/test_core.py#L1710Lib/test/test_zipfile/test_core.py#L2374

底层原理:_ZipRepacker 如何安全地搬移字节

repack() 的实际工作委托给内部类 _ZipRepacker(定义于 Lib/zipfile/init.py#L1437),其 repack(zfile, removed) 方法(Lib/zipfile/init.py#L1449)实现了整套字节搬移算法。理解它有助于你判断何时调用安全、何时可能抛出异常:

  1. 按偏移排序并校验重叠:将 filelistremoved 合并后按 header_offset 排序,逐条计算每个条目在文件中的名义大小(entry_size,即到下一条目头部或 start_dir 的距离),再用 _calc_local_file_entry_size 按本地文件头实际解析出的真实大小(used_entry_size)做对比。若 used_entry_size > entry_size,说明条目相互重叠,无法保证安全搬移,直接抛出 BadZipFile("Overlapped entries: ...")Lib/zipfile/init.py#L1552-L1554)。对应测试 test_repack_overlapping_blocksLib/test/test_zipfile/test_core.py#L2135

  2. 保守的剥离策略(防误删):算法刻意只剥离"看起来像连续本地条目序列"的冗余字节,对前后夹着随机字节的情况选择保留而非冒险删除。文档字符串(Lib/zipfile/init.py#L1457-L1469)明确说明:这样设计是为了防止意外删除数据(false positive),代价是某些罕见场景下可能出现漏删(false negative)。_validate_local_file_entry_sequenceLib/zipfile/init.py#L1662)从指定偏移开始逐条校验本地条目,直到遇到无法解析为合法本地文件头的位置为止,并带偏移缓存以提升性能。

  3. 区分两种调用路径

    • removed=None(全面扫描):用 _calc_initial_entry_offsetLib/zipfile/init.py#L1625)在第一个被引用条目之前扫描 PK\x03\x04 文件头签名(stringFileHeader),若能确认前置数据是一串连续的本地条目,就把这段整体当作可剥离的起始偏移;同时还会剥离被引用条目之间经校验为连续条目的间隙。
    • 提供 removed(定向剥离):起始偏移固定为 0,只针对 removed 集合中的条目执行搬移——把这些条目之后紧跟的数据前移,覆盖被删条目的字节,见 Lib/zipfile/init.py#L1581-L1590
  4. 数据描述符的处理:对使用数据描述符的条目(如流式写入产生、CRC 与大小在数据之后才确定的条目),本地头部中的大小字段为 0,必须通过扫描确定描述符位置与条目真实边界。_scan_data_descriptorLib/zipfile/init.py#L1754)按 0x08074b50 签名扫描;strict_descriptor=False 时还可用 _scan_data_descriptor_no_sig(无签名扫描)与 _scan_data_descriptor_no_sig_by_decompression(边解压边追踪压缩块结束位置,见 Lib/zipfile/init.py#L1804)。对应测试 test_repack_scan_unsigned_data_descriptorLib/test/test_zipfile/test_core.py#L2150

  5. 分块搬移与截断_copy_bytesLib/zipfile/init.py#L1879)按 chunk_size 分块执行 seek + read + write + flush,搬移后更新每个 ZipInfo.header_offsetstart_dir 并置 _didModify = TrueLib/zipfile/init.py#L1618-L1623)。最终在 close() 时,ZipFile 会依据 _didModify 重写中央目录(Lib/zipfile/init.py#L2664),文件尾部的冗余字节由 'a' 模式关闭流程截断。

官方测试覆盖:验证可靠性的证据

标准库测试文件 Lib/test/test_zipfile/test_core.py 为这两个新方法提供了系统性的回归保障(该文件中与 remove/repack 相关的用例超过 40 个),可以从测试命名与断言中快速理解各边界行为:

测试类别 代表用例 验证点
按名字删除 test_remove_by_nameL1502 infolist() 正确收缩、NameToInfo 缓存同步、删除后 testzip() 通过
按 ZipInfo 删除 test_remove_by_zinfoL1523 传入 ZipInfo 对象同样生效
不存在条目 test_remove_by_name_nonexistL1544 抛出 KeyError
重名条目 test_remove_by_name_duplicatedL1556 删除其中一个后 NameToInfo 仍指向剩余同名条目,可连续删除多个同名成员
模式/状态约束 test_remove_mode_rL1724)、test_remove_closedL1710 'r' 模式、已关闭归档均拒绝调用
repack 基础 test_repack_basicL1801)、test_repack_propagationL1832 删除成员 + repack 后文件内容正确、可正常打开
残留字节场景 test_repack_bytes_before_first_fileL1852)、test_repack_bytes_after_removed_filesL2012)、test_repack_bytes_between_removed_filesL2056 冗余字节位于文件前/后/中间时均不误删真实数据
重叠/坏偏移 test_repack_overlapping_blocksL2135)、test_repack_removed_bad_header_offsetL2300 条目重叠或头部偏移损坏时安全报错
无签名描述符 test_repack_scan_unsigned_data_descriptorL2150 strict_descriptor=False 的降级路径
模式约束 test_repack_mode_rL2404)、test_repack_mode_wL2412)、test_repack_mode_xL2419 'a' 模式一律拒绝

适用场景与使用注意事项

推荐场景

  • 向既有归档追加数据后删除旧版本成员,例如更新 .whl.egg 或自定义存档格式中的单个文件;
  • 清理归档中不应发布的文件(密钥、缓存、.pyc)而不重建整个归档;
  • 作为构建流水线的一步:'a' 模式打开 → 删除 → repack() → 关闭,全程不产生临时归档副本。

注意事项

  1. repack() 仅支持以 'a' 模式打开的真实文件;若用 'w'/'x' 模式或非 seek 的流式缓冲打开,会直接抛出 ValueError。要原地修改归档,请始终使用 with zipfile.ZipFile(path, 'a') as zf:
  2. remove() 本身只改索引与中央目录,不回收空间;期望文件变小必须显式调用 repack(),或在下一次调用 repack() 时传入先前 remove() 的返回值以精确剥离。
  3. repack() 是对整个文件的数据搬移操作,时间复杂度与归档中数据总量成正比;对大归档建议接受默认的 chunk_size=2**20,它会按 1 MiB 分块读写,避免一次性占用过多内存。
  4. 归档内存在多个打开句柄时(_fileRefCnt > 1)不能 repack;请在调用前确保所有读取/写入句柄均已关闭。
  5. 算法对"冗余字节紧邻本地条目且可被解析为连续条目序列"的情况执行剥离,对无法确认的随机字节采取保留策略,因此个别极端构造的归档可能出现空间未能完全回收(false negative),但不会误删数据(false positive)——这是文档字符串中明确声明的安全取舍(Lib/zipfile/init.py#L1468-L1469)。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525