Python中的压缩编解码器如何工作?

8 投票
4 回答
1828 浏览
提问于 2025-04-16 04:45

我正在用Python查询一个数据库,并把结果存档到日志文件里,同时想把数据压缩一下。不过,我遇到了一些问题。

我的代码是这样的:

log_file = codecs.open(archive_file, 'w', 'bz2')
for id, f1, f2, f3 in cursor:
    log_file.write('%s %s %s %s\n' % (id, f1 or 'NULL', f2 or 'NULL', f3))

但是,我输出的文件大小是1,409,780。用命令 bunzip2 解压这个文件后,得到的文件大小是943,634,再用 bzip2 压缩一次,文件大小变成了217,275。换句话说,未压缩的文件比用Python的bzip编码压缩的文件要小得多。 有没有办法解决这个问题,除了在命令行上运行 bzip2

我试过用Python的gzip编码(把那行改成 codecs.open(archive_file, 'a+', 'zip')),看看能否解决这个问题。虽然文件还是很大,但当我尝试解压这个文件时,出现了 gzip: archive_file: not in gzip format 的错误。 这是怎么回事呢?


编辑:我最开始是以追加模式打开文件,而不是写入模式。虽然这可能不是问题,但如果文件是以'w'模式打开,问题依然存在。

4 个回答

0

这个问题是因为你使用了追加模式,这样会导致文件里包含多个压缩的数据块。我们来看一个例子:

>>> import codecs
>>> with codecs.open("myfile.zip", "a+", "zip") as f:
>>>     f.write("ABCD")

在我的系统上,这样生成的文件大小是12个字节。我们来看看里面有什么:

>>> with codecs.open("myfile.zip", "r", "zip") as f:
>>>     f.read()
'ABCD'

好,现在我们再用追加模式写一次:

>>> with codecs.open("myfile.zip", "a+", "zip") as f:
>>>     f.write("EFGH")

这个文件现在变成了24个字节,里面的内容是:

>>> with codecs.open("myfile.zip", "r", "zip") as f:
>>>     f.read()
'ABCD'

这里发生的事情是,解压工具期待的是一个单一的压缩流。你需要查看相关的规范,了解多个连接的流的官方处理方式,但根据我的经验,它们只会处理第一个流,其他的数据会被忽略。这就是Python的处理方式。

我猜bunzip2也是这样做的。所以实际上你的文件是被压缩过的,大小比里面的数据要小得多。但是当你用bunzip2来处理它时,你只得到了你第一次写入的那一组记录;其他的部分则被丢弃了。

1

问题似乎出在每次调用write()的时候,都会写入输出。这导致每一行都被压缩成自己的bzip块。

我建议你在写入文件之前,先在内存中构建一个更大的字符串(如果你担心性能,也可以用字符串列表)。一个不错的目标大小是900K(或者更多),因为这是bzip2使用的块大小。

2

正如其他人提到的,问题在于 codecs 库没有使用增量编码器来处理数据;相反,它会把每次传给 write 方法的数据片段都当作一个压缩块来编码。这种做法非常低效,而且对于一个设计用来处理流的库来说,这真是个糟糕的设计选择。

有趣的是,Python 里其实已经内置了一个完全合理的增量 bz2 编码器。创建一个“像文件一样”的类,让它自动做正确的事情并不难。

import bz2

class BZ2StreamEncoder(object):
    def __init__(self, filename, mode):
        self.log_file = open(filename, mode)
        self.encoder = bz2.BZ2Compressor()

    def write(self, data):
        self.log_file.write(self.encoder.compress(data))

    def flush(self):
        self.log_file.write(self.encoder.flush())
        self.log_file.flush()

    def close(self):
        self.flush()
        self.log_file.close()

log_file = BZ2StreamEncoder(archive_file, 'ab')

需要注意的是:在这个例子中,我是以追加模式打开文件的;将多个压缩流追加到一个文件中,使用 bunzip2 是完全没问题的,但 Python 自身处理起来就不行(虽然有一个 补丁 可以解决这个问题)。如果你需要把自己创建的压缩文件再读回 Python,最好每个文件只用一个流。

撰写回答