如何处理包含额外数据的Gzip文件?

16 投票
4 回答
12456 浏览
提问于 2025-04-16 11:25

我正在写一个脚本,用来处理来自仪器的数据,这些数据是以gzip格式的流传输的。在大约90%的情况下,gzip模块运行得很好,但有些流会导致它报错,显示IOError: Not a gzipped file。如果我去掉gzip的头部,直接把解压流传给zlib,我会得到Error -3 while decompressing data: incorrect header check的错误。经过大约半天的努力,我发现那些有问题的流在末尾附加了一些看似随机的额外字节(这些字节并不是gzip数据的一部分)。

我觉得Python无法处理这些文件有两个原因很奇怪:

  1. Gzip和7zip都能顺利打开这些“填充”的文件。(Gzip会显示decompression OK, trailing garbage ignored的消息,而7zip则默默成功。)
  2. Gzip和Python的文档似乎都表示这应该是可以的:(我强调的部分)

    Gzip的format.txt

    必须能够使用任何压缩方法检测压缩数据的结束,无论压缩数据的实际大小如何。特别是,解压缩器必须能够检测并跳过附加到有效压缩文件的额外数据,无论是在记录导向的文件系统上,还是当压缩数据只能以某个块大小的倍数从设备读取时。

    Python的gzip.GzipFile

    调用GzipFile对象的close()方法并不会关闭fileobj因为你可能希望在压缩数据后追加更多内容。这也允许你将一个为写入打开的StringIO对象作为fileobj传入,并使用StringIO对象的getvalue()方法获取结果的内存缓冲区。

    Python的zlib.Decompress.unused_data

    一个字符串,包含压缩数据结束后的任何字节。也就是说,这个字符串在最后一个包含压缩数据的字节可用之前保持为""如果整个字符串实际上包含压缩数据,那么这个字符串就是"",空字符串。

    确定压缩数据字符串结束的唯一方法是实际解压缩它。这意味着当压缩数据是更大文件的一部分时,你只能通过读取数据并将其与某个非空字符串一起传入解压缩对象的decompress()方法,直到unused_data属性不再是空字符串,来找到它的结束位置。

以下是我尝试的四种方法。(这些例子是Python 3.1,但我在2.5和2.7中测试时也遇到了同样的问题。)

# approach 1 - gzip.open
with gzip.open(filename) as datafile:
    data = datafile.read()

# approach 2 - gzip.GzipFile
with open(filename, "rb") as gzipfile:
    with gzip.GzipFile(fileobj=gzipfile) as datafile:
        data = datafile.read()

# approach 3 - zlib.decompress
with open(filename, "rb") as gzipfile:
    data = zlib.decompress(gzipfile.read()[10:])

# approach 4 - zlib.decompressobj
with open(filename, "rb") as gzipfile:
    decompressor = zlib.decompressobj()
    data = decompressor.decompress(gzipfile.read()[10:])

我是不是做错了什么?

更新

好的,虽然gzip的问题似乎是模块中的一个bug,但我在zlib上的问题是我自己造成的。;-)

在深入研究gzip.py时,我意识到我做错了什么——默认情况下,zlib.decompress等方法期望的是zlib包装的流,而不是裸的deflate流。通过传入一个负值给wbits,你可以告诉zlib跳过zlib头部,解压原始流。这两种方法都有效:

# approach 5 - zlib.decompress with negative wbits
with open(filename, "rb") as gzipfile:
    data = zlib.decompress(gzipfile.read()[10:], -zlib.MAX_WBITS)

# approach 6 - zlib.decompressobj with negative wbits
with open(filename, "rb") as gzipfile:
    decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
    data = decompressor.decompress(gzipfile.read()[10:])

4 个回答

1

我也遇到过这个问题,但这些回答都没有解决我的困扰。所以,我是这样解决这个问题的:

#for gzip files
unzipped = zlib.decompress(gzip_data, zlib.MAX_WBITS|16)

#for zlib files
unzipped = zlib.decompress(gzip_data, zlib.MAX_WBITS)


#automatic header detection (zlib or gzip):
unzipped = zlib.decompress(gzip_data, zlib.MAX_WBITS|32)

根据你的具体情况,可能需要对你的数据进行解码,比如:

unzipped = unzipped.decode()

https://docs.python.org/3/library/zlib.html

5

我以前也遇到过类似的问题。我写了一个新模块,这个模块在处理数据流时效果更好。你可以试试看这个模块,看看它是否适合你。

22

这是一个bug。Python中的gzip模块质量远远达不到标准库应该有的水平。

问题在于,gzip模块假设文件是一个gzip格式文件的流。在压缩数据的末尾,它会重新开始,期待一个新的gzip头;如果没有找到,它就会抛出一个异常。这是错误的。

当然,把两个gzip文件连接在一起是有效的,比如:

echo testing > test.txt
gzip test.txt
cat test.txt.gz test.txt.gz > test2.txt.gz
zcat test2.txt.gz
# testing
# testing

gzip模块的错误在于,如果第二次没有gzip头,它不应该抛出异常;它应该直接结束文件。它应该只在第一次没有头的时候抛出异常。

没有简单的方法可以解决这个问题,除非直接修改gzip模块;如果你想这样做,可以看看_read方法的底部。它应该设置另一个标志,比如reading_second_block,来告诉_read_gzip_header抛出EOFError而不是IOError

这个模块还有其他bug。例如,它不必要地进行寻址,这导致在不可寻址的流(比如网络套接字)上失败。这让我对这个模块的信心很低:一个不知道gzip需要在不寻址的情况下工作的开发者,根本不适合为Python标准库实现这个功能。

撰写回答