奇怪的“BadZipfile: Bad CRC-32”问题

13

这段代码是一个 Django 应用程序中的简化版本,它通过 HTTP 多部分 POST 接收上传的 zip 文件,并对其中的数据进行只读处理:

#!/usr/bin/env python

import csv, sys, StringIO, traceback, zipfile
try:
    import io
except ImportError:
    sys.stderr.write('Could not import the `io` module.\n')

def get_zip_file(filename, method):
    if method == 'direct':
        return zipfile.ZipFile(filename)
    elif method == 'StringIO':
        data = file(filename).read()
        return zipfile.ZipFile(StringIO.StringIO(data))
    elif method == 'BytesIO':
        data = file(filename).read()
        return zipfile.ZipFile(io.BytesIO(data))


def process_zip_file(filename, method, open_defaults_file):
    zip_file    = get_zip_file(filename, method)
    items_file  = zip_file.open('items.csv')
    csv_file    = csv.DictReader(items_file)

    try:
        for idx, row in enumerate(csv_file):
            image_filename = row['image1']

            if open_defaults_file:
                z = zip_file.open('defaults.csv')
                z.close()

        sys.stdout.write('Processed %d items.\n' % idx)
    except zipfile.BadZipfile:
        sys.stderr.write('Processing failed on item %d\n\n%s' 
                         % (idx, traceback.format_exc()))


process_zip_file(sys.argv[1], sys.argv[2], int(sys.argv[3]))

很简单。我们打开zip文件并打开其中一个或两个CSV文件。

奇怪的是,如果我使用较大的zip文件(约13 MB)并从StringIO.StringIOio.BytesIO实例化ZipFile(可能是除普通文件名之外的任何内容?当我尝试使用TemporaryUploadedFile或通过调用os.tmpfile()shutil.copyfileobj()创建的文件对象来创建ZipFile时,在Django应用程序中出现了类似的问题),并且打开两个CSV文件而不仅仅是一个,那么在处理的最后阶段会失败。这是我在Linux系统上看到的输出:

$ ./test_zip_file.py ~/data.zip direct 1
Processed 250 items.

$ ./test_zip_file.py ~/data.zip StringIO 1
Processing failed on item 242

Traceback (most recent call last):
  File "./test_zip_file.py", line 26, in process_zip_file
    for idx, row in enumerate(csv_file):
  File ".../python2.7/csv.py", line 104, in next
    row = self.reader.next()
  File ".../python2.7/zipfile.py", line 523, in readline
    return io.BufferedIOBase.readline(self, limit)
  File ".../python2.7/zipfile.py", line 561, in peek
    chunk = self.read(n)
  File ".../python2.7/zipfile.py", line 581, in read
    data = self.read1(n - len(buf))
  File ".../python2.7/zipfile.py", line 641, in read1
    self._update_crc(data, eof=eof)
  File ".../python2.7/zipfile.py", line 596, in _update_crc
    raise BadZipfile("Bad CRC-32 for file %r" % self.name)
BadZipfile: Bad CRC-32 for file 'items.csv'

$ ./test_zip_file.py ~/data.zip BytesIO 1
Processing failed on item 242

Traceback (most recent call last):
  File "./test_zip_file.py", line 26, in process_zip_file
    for idx, row in enumerate(csv_file):
  File ".../python2.7/csv.py", line 104, in next
    row = self.reader.next()
  File ".../python2.7/zipfile.py", line 523, in readline
    return io.BufferedIOBase.readline(self, limit)
  File ".../python2.7/zipfile.py", line 561, in peek
    chunk = self.read(n)
  File ".../python2.7/zipfile.py", line 581, in read
    data = self.read1(n - len(buf))
  File ".../python2.7/zipfile.py", line 641, in read1
    self._update_crc(data, eof=eof)
  File ".../python2.7/zipfile.py", line 596, in _update_crc
    raise BadZipfile("Bad CRC-32 for file %r" % self.name)
BadZipfile: Bad CRC-32 for file 'items.csv'

$ ./test_zip_file.py ~/data.zip StringIO 0
Processed 250 items.

$ ./test_zip_file.py ~/data.zip BytesIO 0
Processed 250 items.

顺便提一下,在我的OS X系统上,代码在相同的条件下以不同的方式失败。它似乎读取了损坏的数据并变得非常混乱,而不是抛出BadZipFile异常。

这一切都让我觉得我在代码中做了一些不应该做的事情——比如在已经打开同一zip文件对象中有另一个文件的情况下调用zipfile.open?使用ZipFile(filename)时似乎没有问题,但当传递类似于文件的对象给ZipFile时,可能会出现问题,因为在zipfile模块中存在一些实现细节吗?

也许我在zipfile文档中漏掉了什么?或者可能还未记录?或者(最不可能),这是zipfile模块中的一个错误?

4个回答

18

我可能已经找到了问题和解决方案,但不幸的是我不得不用我自己的修改版(这里叫做myzipfile)替换掉Python的zipfile模块。

$ diff -u ~/run/lib/python2.7/zipfile.py myzipfile.py
--- /home/msabramo/run/lib/python2.7/zipfile.py 2010-12-22 17:02:34.000000000 -0800
+++ myzipfile.py        2011-04-11 11:51:59.000000000 -0700
@@ -5,6 +5,7 @@
 import binascii, cStringIO, stat
 import io
 import re
+import copy

 try:
     import zlib # We may need its compression method
@@ -877,7 +878,7 @@
         # Only open a new file for instances where we were not
         # given a file object in the constructor
         if self._filePassed:
-            zef_file = self.fp
+            zef_file = copy.copy(self.fp)
         else:
             zef_file = open(self.filename, 'rb')
标准的 zipfile 模块存在的问题在于,当传入一个文件对象(而不是文件名)时,它会将同一个传入的文件对象用于每次调用 open 方法。这意味着 tellseek 会在同一个文件上调用,因此尝试在 zip 文件中打开多个文件会导致文件位置共享,从而导致多个 open 调用相互干扰。相比之下,当传入文件名时,open 会打开一个新的文件对象。我的解决方案是,在传入文件对象的情况下,不直接使用该文件对象,而是创建其副本。
这对 zipfile 的更改解决了我遇到的问题。
$ ./test_zip_file.py ~/data.zip StringIO 1
Processed 250 items.

$ ./test_zip_file.py ~/data.zip BytesIO 1
Processed 250 items.

$ ./test_zip_file.py ~/data.zip direct 1
Processed 250 items.

但我不知道这是否会对zipfile产生其他负面影响...

编辑:我刚刚在Python文档中发现了这个提及,以前我不知道。在http://docs.python.org/library/zipfile.html#zipfile.ZipFile.open上,它说:

注意:如果ZipFile是通过将文件对象作为构造函数的第一个参数传递而创建的,则open()返回的对象共享ZipFile的文件指针。在这些情况下,在对ZipFile对象执行任何其他操作之后,不应再使用open()返回的对象。如果ZipFile是通过将字符串(即文件名)作为构造函数的第一个参数传递而创建的,则open()将创建一个新的文件对象,该文件对象将由ZipExtFile保持,使其能够独立于ZipFile运行。


我刚在 Python 文档中发现了这个提及,之前不知道为什么漏过了它。 - Marc Abramowitz
我遇到了同样的问题。相同的代码在Python2中产生错误,但在Python3中没有。所以我按照这里的解决方案尝试传递文件名而不是文件对象。然后我遇到了“打开的文件太多”的错误,如果zip文件有数百个成员,则可能会发生这种情况。然后我意识到,在初始化“ZipFile”之前对文件对象执行“seek(0)”,错误就消失了。不知道为什么,因为我之前没有对文件对象做任何操作... - deeenes
我遇到了相同的错误信息,但在我的情况下,这只是因为我有两个线程同时尝试从同一个ZipFile对象中提取文件。显然,这样做是行不通的,添加锁定解决了问题。 - jlh

2

1
在我的情况下,这解决了问题:

pip uninstall pillow

1
由于某种原因,这解决了我使用openpyxl的问题。 - Luiz Domingues

0

你是不是在桌面上打开了它?有时候我也会遇到这种情况,解决方法就是在 Python 会话中运行代码,而不要在文件外打开。


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