Python中的压缩编解码器如何工作?
我正在用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 个回答
这个问题是因为你使用了追加模式,这样会导致文件里包含多个压缩的数据块。我们来看一个例子:
>>> 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来处理它时,你只得到了你第一次写入的那一组记录;其他的部分则被丢弃了。
问题似乎出在每次调用write()
的时候,都会写入输出。这导致每一行都被压缩成自己的bzip块。
我建议你在写入文件之前,先在内存中构建一个更大的字符串(如果你担心性能,也可以用字符串列表)。一个不错的目标大小是900K(或者更多),因为这是bzip2使用的块大小。
正如其他人提到的,问题在于 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,最好每个文件只用一个流。