使用ZipFile模块从zip文件中删除文件

48

我找到的唯一从zip文件中删除文件的方法是创建一个没有要删除文件的临时zip文件,然后将其重命名为原始文件名。

在Python 2.4中,ZipInfo类有一个属性file_offset,因此可以创建第二个zip文件并将数据复制到其他文件而无需解压/重新压缩。

Python 2.6中缺少file_offset,那么除了通过解压每个文件然后重新压缩它来创建另一个zip文件之外,是否还有其他选择?

也许有一种直接的方法可以删除zip文件中的文件,但我搜索了一下并没有找到任何信息。


我在Python错误跟踪器上找到了这个讨论如何从zip文件中删除文件的线程:https://bugs.python.org/issue6818 - Elias Zamaria
5个回答

55

下面的代码片段对我有效(从Zip归档中删除所有*.exe文件):

zin = zipfile.ZipFile ('archive.zip', 'r')
zout = zipfile.ZipFile ('archve_new.zip', 'w')
for item in zin.infolist():
    buffer = zin.read(item.filename)
    if (item.filename[-4:] != '.exe'):
        zout.writestr(item, buffer)
zout.close()
zin.close()

如果你将所有东西都读入内存,就可以消除对第二个文件的需要。但是,这段代码重新压缩了所有内容。

经过仔细检查,ZipInfo.header_offset 是相对于文件开头的偏移量。名称有点误导,但是主 Zip 头实际上存储在文件结尾处。我的十六进制编辑器证实了这一点。

因此,你会遇到以下问题:你需要删除主头中的目录条目,否则它将指向一个不存在的文件。如果保留要删除的文件的本地标头,保持主头不变可能会起作用,但我不确定。你以前如何使用旧模块进行操作的?

如果不修改主头,打开时会出现“zipfile 中缺少 X 字节”的错误。 这篇文章 可能会帮助你找出如何修改主头。


2
谢谢,但如果我没错的话 - 当您查看zipfile.writestr时,您会发现这只是一个重新压缩。直接复制已经压缩的文件而不进行解压缩然后再次压缩会更快。 - RSabet
@RSabt 我同意 mdm 的观点,解压和重新压缩是目前唯一可行的选择。顺便说一下,当你要做更严肃的事情时最好使用 os.path.splitext(),虽然 mdm 的代码有所帮助。 - RayLuo
1
同时,您可以避免提取可执行文件。首先检查名称,如果不是可执行文件,则读取输入。这将节省一些无用的提取时间。 - Jean-François Fabre

10

虽不太优雅,但这就是我做的方式:

import subprocess
import zipfile

z = zipfile.ZipFile(zip_filename)

files_to_del = filter( lambda f: f.endswith('exe'), z.namelist()]

cmd=['zip', '-d', zip_filename] + files_to_del
subprocess.check_call(cmd)

# reload the modified archive
z = zipfile.ZipFile(zip_filename)

1
这就是我最终所做的。很丑,但是 ZipFile 似乎没有删除或更新/替换文件的方法。 - ArtOfWarfare
1
此解决方案是特定于平台的和/或需要在操作系统上安装zip软件。此外,引入了新子进程的开销。 - Buzz

7

基于Elias Zamaria对该问题的评论。

阅读了Python-问题 #51067后,我想对它进行更新。

目前已经有解决方案,但由于作者缺少贡献者协议,因此未被Python批准。

尽管如此,您可以从https://github.com/python/cpython/blob/659eb048cc9cac73c46349eb29845bc5cd630f09/Lib/zipfile.py获取代码,并从中创建一个单独的文件。然后只需从项目中引用该文件而不是内置的Python库:import myproject.zipfile as zipfile

使用:

with zipfile.ZipFile(f"archive.zip", "a") as z:
    z.remove(f"firstfile.txt")

我相信它将被包含在未来的Python版本中。对于我的使用情况而言,它能够完美地工作。


似乎对.jar文件有问题,有时会删除你想要的文件而不是它本身。 - Maksiks

6

来自ruamel.std.zipfile¹的例程delete_from_zip_file允许您根据ZIP文件中文件的完整路径或基于(re)模式删除文件。例如,您可以使用以下命令从test.zip中删除所有.exe文件:

from ruamel.std.zipfile import delete_from_zip_file

delete_from_zip_file('test.zip', pattern='.*.exe')  

请注意*前面的点号。
这个方案类似于mdm的解决方案(包括需要重新压缩文件),但是它使用InMemZipFile()类在内存中重新创建ZIP文件,在完全读取旧文件后覆盖旧文件。
¹ 免责声明:我是该软件包的作者。

delete_from_zip_file例程对我非常有用,但是当我尝试从一个包含许多文件和文件夹的大型存档(大小约为3GB)中删除许多文件时,我遇到了这个错误:“LargeZipFile:Zipfile size would require ZIP64 extensions”。我猜想在ruamel.std.zipfile中应该进行修改,在__init__.py文件中(例如允许Zip64 = True for zipfile.ZipFile(..)),对吗? - lugger1
我从未使用过 allowZip64,不知道它是关于什么的。 - Anthon
1
小问题的最简单解决方案 - Maksiks

0

简而言之:

import zipfile

with zipfile.ZipFile("bad.zip") as bad:
    # Or use "a" instead of "w" if you're appending
    with zipfile.ZipFile("good", "w") as good:
        for zip_info in bad.infolist():
            # I had hundreds of duplications of 'sample_100.csv'
            not_a_bad_file = zip_info.filename != 'sample_33.csv' or zip_info.file_size > 146622
            if not_a_bad_file:
                good.writestr(zip_info, bad.read(zip_info))

解释:
我不小心添加了多个同名的文件,而且它们的大小都接近于0字节。@mdm建议的方法在这里行不通。这是因为如果你将文件名(str)传递给read方法,它会给你最后一个项目 - 至少看起来是这样。然而,在阅读CPython代码中的库文档之后,这一部分将变得明显起来。
.. note::

      The :meth:`.open`, :meth:`read` and :meth:`extract` methods can take a filename
      or a :class:`ZipInfo` object.  You will appreciate this when trying to read a
      ZIP file that contains members with duplicate names.

通过传递zip_info(一个ZipInfo对象),您可以确保检索到确切的文件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接