奇怪的"BadZipfile:错误的CRC-32"问题

Mar*_*itz 5 zipfile bytesio stringio

此代码简化了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]))
Run Code Online (Sandbox Code Playgroud)

很简单.我们在zip文件中打开zip文件和一个或两个CSV文件.

有什么奇怪的是,如果我跑这一个大的zip文件(〜13 MB),并把它实例化ZipFile从一个StringIO.StringIO或一个io.BytesIO(也许什么比一个普通的文件名等?我要创建一个当有在Django应用程序类似的问题ZipFile,从a TemporaryUploadedFile或甚至通过调用os.tmpfile()和创建的文件对象,shutil.copyfileobj()并让它打开两个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.
Run Code Online (Sandbox Code Playgroud)

顺便提一下,代码在相同条件下失败但在我的OS X系统上以不同的方式失败.BadZipfile它似乎是读取损坏的数据而非常混淆,而不是异常.

这一切都告诉我,我在这段代码中做了一些你不应该做的事情 - 例如:zipfile.open在同一个zip文件对象中打开另一个文件时调用文件?这在使用时似乎不是问题ZipFile(filename),但是在传递类ZipFile文件对象时可能会出现问题,因为zipfile模块中有一些实现细节?

也许我错过了zipfile文档中的内容?或许它还没有记录?或者(最不可能),zipfile模块中的错误?

Mar*_*itz 11

我可能刚刚发现了问题和解决方案,但不幸的是我不得不zipfile用我自己的一个黑客(myzipfile在这里调用)替换Python的模块.

$ 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')
Run Code Online (Sandbox Code Playgroud)

标准zipfile模块中的问题是,当传递文件对象(不是文件名)时,它会对open方法的每次调用使用相同的传入文件对象.这意味着tell并且seek正在调用同一个文件,因此尝试在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.
Run Code Online (Sandbox Code Playgroud)

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

编辑:我刚刚在Python文档中发现了这一点,我之前曾忽略过这一点.在http://docs.python.org/library/zipfile.html#zipfile.ZipFile.open上,它说:

注意:如果ZipFile是通过传入类似文件的对象作为构造函数的第一个参数创建的,那么返回的对象将open()共享ZipFile的文件指针.在这些情况下,open()在对ZipFile对象执行任何其他操作后,不应使用返回的对象.如果ZipFile是通过传入一个字符串(文件名)作为构造函数的第一个参数open()创建的,那么将创建一个由ZipExtFile保存的新文件对象,允许它独立于ZipFile运行.