打开不支持压缩类型的zip文件时,会静默返回空的文件流而不是抛出异常。

16
似乎遇到了一个新手错误,但我不是新手。 我有一个大小为1.2G的压缩文件'train.zip',其中包含一个3.5G的'train.csv'文件。 我打开了压缩文件和文件本身,没有出现任何异常(没有LargeZipFile),但结果文件流似乎为空。(UNIX 'unzip -c ...' 确认它是好的) Python ZipFile.open()返回的文件对象不能被定位或告知,所以我无法检查它们。 Python分发版是2.7.3 EPD-free 7.3-1 (32-bit);但应该适用于大型压缩文件。操作系统是MacOS 10.6.6。
import csv
import zipfile as zf

zip_pathname = os.path.join('/my/data/path/.../', 'train.zip')
#with zf.ZipFile(zip_pathname).open('train.csv') as z:
z = zf.ZipFile(zip_pathname, 'r', zf.ZIP_DEFLATED, allowZip64=True) # I tried all permutations
z.debug = 1
z.testzip() # zipfile integrity is ok

z1 = z.open('train.csv', 'r') # our file keeps coming up empty?

# Check the info to confirm z1 is indeed a valid 3.5Gb file...
z1i = z.getinfo(file_name)
for att in ('filename', 'file_size', 'compress_size', 'compress_type', 'date_time',  'CRC', 'comment'):
    print '%s:\t' % att, getattr(z1i,att)
# ... and it looks ok. compress_type = 9 ok?
#filename:  train.csv
#file_size: 3729150126
#compress_size: 1284613649
#compress_type: 9
#date_time: (2012, 8, 20, 15, 30, 4)
#CRC:   1679210291

# All attempts to read z1 come up empty?!
# z1.readline() gives ''
# z1.readlines() gives []
# z1.read() takes ~60sec but also returns '' ?

# code I would want to run is:
reader = csv.reader(z1)
header = reader.next()
return reader
4个回答

22
造成这种情况的原因是以下两点:
  • 此文件的压缩类型为类型9:Deflate64/Enhanced Deflate(PKWare的专有格式,而不是更常见的类型8)
  • 还有一个zipfile漏洞:它不会针对不支持的压缩类型抛出异常。以前它只会静默返回错误的文件对象 [第4.4.5节 压缩方法]。太糟糕了!更新:我提交了问题14313,并在2012年修复,所以现在当压缩类型未知时,它会引发NotImplementedError。

一种命令行解决方法是先解压缩,然后重新压缩为普通的类型8:deflated

zipfile将在2.7、3.2+中引发异常,我想由于法律原因,zipfile永远无法实际处理类型9。

Python文档完全没有提到zipfile不能处理其他压缩类型 :(


你知道如何检查压缩类型,以便预测静默失败吗? - Martin Taleski
1
@MartinTaleski:我提交了一个错误报告,他们解决了它,现在当压缩类型未知时会引发NotImplementedError。你可以使用try..catch来处理它。EAFP哲学。 - smci

5

对于Python的ZipFile不支持的压缩类型,我的解决方案是在ZipFile.extractall失败时调用7zip。

from zipfile import ZipFile
import subprocess, sys

def Unzip(zipFile, destinationDirectory):
    try:
        with ZipFile(zipFile, 'r') as zipObj:
            # Extract all the contents of zip file in different directory
            zipObj.extractall(destinationDirectory)
    except:
        print("An exception occurred extracting with Python ZipFile library.")
        print("Attempting to extract using 7zip")
        subprocess.Popen(["7z", "e", f"{zipFile}", f"-o{destinationDirectory}", "-y"])

很好。我知道这不适用于每个人,但对于我的使用情况来说,这是一个完美的解决方案。谢谢! - Jonathan B.
谢谢!请注意,在 POSIX 系统上,将带有参数的字符串传递给 Popen 构造函数会失败,因此为了实现跨平台,应该传递一个序列而不是一个字符串,例如 subprocess.Popen(["7z", "e", f"{zipFile}", f"-o{destinationDirectory}", "-y"])。请参见此处的讨论 https://docs.python.org/3.8/library/subprocess.html#popen-constructor - Hugo
1
不错。你假设 7z 在路径上某个地方,用户可能想添加逻辑来加强对于 FileNotFoundError: [Errno 2] No such file or directory 的防护。 - smci

2
如果问题是因为Python标准库不支持Deflate64算法,现在有一个名为“zipfile-deflate64”的包可用。
它仍然被列为“alpha”阶段。我昨天(2022年7月18日)刚开始使用它,对我来说做得很好。
使用它非常容易,导入它使您可以像通常使用zipfile库一样使用它,并添加了对Deflate64的支持。 pypi上“zipfile-deflate64”包的链接 GitHub上“zipfile-deflate64”项目的链接 以下是如何使用它的示例。 API与内置的zipfile包相同:
import zipfile_deflate64 as zipfile

tag_hist_path = "path\\to\\your\\zipfile.zip"
parentZip = zipfile.ZipFile(tag_hist_path, mode="r", compression=zipfile.ZIP_DEFLATED64)
fileNames = [f.filename for f in parentZip.filelist]
memberArchive = parentZip.open(fileNames[0], mode="r")
b = memberArchive.read() #reading all bytes at once, assuming file isn't too big
txt = b.decode("utf-8") #decode bytes to text string
memberArchive.close()
parentZip.close()

这里是一种更简洁、更清晰的处理这样一个存档的方法,根据 @smci 的建议,您不必在出现错误时费力管理流资源(即关闭它们)。
tag_hist_path = "path\\to\\your\\zipfile.zip"
with zipfile.ZipFile(tag_hist_path, mode="r", compression=zipfile.ZIP_DEFLATED64) as parentZip:
    for fileNames in parentZip.filelist:
        with parentZip.open(fileNames[0], mode="r") as memberArchive:
            #Do something with each opened zipfile

好的,你能否发布一下实际代码,展示如何打开一个Deflate64压缩文件? - smci
@smci - 当然。我刚刚添加了一个使用示例。 - BioData41
我会整理你的代码,使用多个with上下文管理器和符合PEP-8规范的名称,如parent_zipmember_archive。此外,我会明确迭代,而不是将fileNames声明为列表推导式,那只会使事情变得更加混乱。同时你也不需要两个显式的close()语句。实际上,你甚至不需要明确声明memberArchiveparentZip作为变量。更短更清晰。 - smci
1
@smci,我添加了一个片段,展示了如何使用with块和迭代文件列表来更成熟地完成这个任务。虽然我没有重命名变量。如果这不正确地捕捉到您的建议意图,请告诉我。 - BioData41
1
做得好,这个包很棒。我希望我能把它作为第二个答案接受。嘿,你的用户是否担心这个包是否合法干净,并且是否得到官方支持?大多数ZIP库已经拒绝实现Deflate64,因为它具有专有性质...最好的希望似乎是zlib的infback9扩展。这是由zlib的原始作者之一Mark Adler在2003年开发的,保存在zlib的源代码库中,但它没有得到官方支持,也不包含构建工具,并且未随zlib包分发。需要更简洁、更清晰。 - smci
显示剩余2条评论

2
“压缩类型9是Deflate64/Enhanced Deflate,Python的zipfile模块不支持(本质上是因为zlib不支持Deflate64,而zipfile委托给了它)。如果较小的文件可以正常工作,我怀疑这个zipfile是由Windows资源管理器创建的:对于较大的文件,Windows资源管理器可以决定使用Deflate64。 (请注意,Zip64与Deflate64不同。Zip64受Python的zipfile模块支持,并且仅对zipfile中存储某些元数据的方式进行了一些更改,但仍然使用常规Deflate进行压缩数据。)但是,stream-unzip现在支持Deflate64。将其示例修改为从本地磁盘读取,并像您的示例一样读取CSV文件:”
import csv
from io import IOBase, TextIOWrapper
import os

from stream_unzip import stream_unzip

def get_zipped_chunks(zip_pathname):
    with open(zip_pathname, 'rb') as f:
       while True:
           chunk = f.read(65536)
           if not chunk:
               break
           yield chunk

def get_unzipped_chunks(zipped_chunks, filename)
    for file_name, file_size, unzipped_chunks in stream_unzip(zipped_chunks):
        if file_name != filename:
            for chunk in unzipped_chunks:
                pass
            continue
        yield from unzipped_chunks

def to_str_lines(iterable):
    # Based on the answer at https://dev59.com/ScPra4cB1Zd3GeqPc0oV#70639580
    chunk = b''
    offset = 0
    it = iter(iterable)

    def up_to_iter(size):
        nonlocal chunk, offset

        while size:
            if offset == len(chunk):
                try:
                    chunk = next(it)
                except StopIteration:
                    break
                else:
                    offset = 0
            to_yield = min(size, len(chunk) - offset)
            offset = offset + to_yield
            size -= to_yield
            yield chunk[offset - to_yield:offset]

    class FileLikeObj(IOBase):
        def readable(self):
            return True
        def read(self, size=-1):
            return b''.join(up_to_iter(float('inf') if size is None or size < 0 else size))

    yield from TextIOWrapper(FileLikeObj(), encoding='utf-8', newline='')

zipped_chunks = get_zipped_chunks(os.path.join('/my/data/path/.../', 'train.zip'))
unzipped_chunks = get_unzipped_chunks(zipped_chunks, b'train.csv')
str_lines = to_str_lines(unzipped_chunks)
csv_reader = csv.reader(str_lines)

for row in csv_reader:
    print(row)

1
这是一种不错的解决方案,可以在不必将整个内容保存在内存中的情况下流式传输。不幸的是,在我的应用程序中,它变得非常缓慢:(...最后,我决定使用命令行解压缩文件到磁盘上,并读取解压缩后的内容。 - Juan A. Navarro
@JuanA.Navarro 是的,deflate64解压缩是纯Python https://github.com/michalc/stream-inflate,所以它慢并不让我感到惊讶。我相信它可以加速,但这可能需要一个项目... - Michal Charemza

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