压缩内存中的文件,计算校验和并将其写入 python 中的 `gzip`

Compress a file in memory, compute checksum and write it as `gzip` in python

我想压缩文件并使用 python 计算压缩文件的校验和。我第一次天真的尝试是使用 2 个函数:

def compress_file(input_filename, output_filename):
    f_in = open(input_filename, 'rb')
    f_out = gzip.open(output_filename, 'wb')
    f_out.writelines(f_in)
    f_out.close()
    f_in.close()


def md5sum(filename):
    with open(filename) as f:
        md5 = hashlib.md5(f.read()).hexdigest()
    return md5

但是会导致压缩文件被写入然后re-read。对于许多文件(> 10 000),每个文件压缩时数 MB,在 NFS 安装的驱动器中,速度很慢。

如何在缓冲区中压缩文件,然后在写入输出文件之前从此缓冲区计算校验和?

文件不是很大,所以我可以负担得起将所有内容都存储在内存中。然而,一个不错的增量版本也可能不错。

最后一个要求是它应该与多处理一起工作(以便并行压缩多个文件)。

我尝试使用 zlib.compress 但返回的字符串缺少 gzip 文件的 header。

编辑:在 之后,我使用了 python3 gzip.compress:

def compress_md5(input_filename, output_filename):
    f_in = open(input_filename, 'rb')
    # Read in buffer
    buff = f_in.read()
    f_in.close()
    # Compress this buffer
    c_buff = gzip.compress(buff)
    # Compute MD5
    md5 = hashlib.md5(c_buff).hexdigest()
    # Write compressed buffer
    f_out = open(output_filename, 'wb')
    f_out.write(c_buff)
    f_out.close()

    return md5

这会生成一个正确的 gzip 文件,但每个文件的输出都不同 运行(md5 不同):

>>> compress_md5('4327_010.pdf', '4327_010.pdf.gz')
'0d0eb6a5f3fe2c1f3201bc3360201f71'
>>> compress_md5('4327_010.pdf', '4327_010.pdf.gz')
'8e4954ab5914a1dd0d8d0deb114640e5'

gzip程序没有这个问题:

 $ gzip -c 4327_010.pdf | md5sum
 8965184bc4dace5325c41cc75c5837f1  -
 $ gzip -c 4327_010.pdf | md5sum
 8965184bc4dace5325c41cc75c5837f1  -

我猜是因为 gzip 模块在创建文件时默认使用当前时间(我猜 gzip 程序使用输入文件的修改)。 gzip.compress.

无法更改

我想在 read/write 模式下创建一个 gzip.GzipFile,控制 mtime,但是 gzip.GzipFile 没有这样的模式。

受到的启发,我编写了以下函数,它在header:

中正确设置了文件名和OS (Unix)
def compress_md5(input_filename, output_filename):
    f_in = open(input_filename, 'rb')    
    # Read data in buffer
    buff = f_in.read()
    # Create output buffer
    c_buff = cStringIO.StringIO()
    # Create gzip file
    input_file_stat = os.stat(input_filename)
    mtime = input_file_stat[8]
    gzip_obj = gzip.GzipFile(input_filename, mode="wb", fileobj=c_buff, mtime=mtime)
    # Compress data in memory
    gzip_obj.write(buff)
    # Close files
    f_in.close()
    gzip_obj.close()
    # Retrieve compressed data
    c_data = c_buff.getvalue()
    # Change OS value
    c_data = c_data[0:9] + '[=15=]3' + c_data[10:]
    # Really write compressed data
    f_out = open(output_filename, "wb")
    f_out.write(c_data)
    # Compute MD5
    md5 = hashlib.md5(c_data).hexdigest()
    return md5

不同运行输出相同。此外 file 的输出与 gzip:

相同
$ gzip -9 -c 4327_010.pdf > ref_max/4327_010.pdf.gz
$ file ref_max/4327_010.pdf.gz 
ref_max/4327_010.pdf.gz: gzip compressed data, was "4327_010.pdf", from Unix, last modified: Tue May  5 14:28:16 2015, max compression
$ file 4327_010.pdf.gz 
4327_010.pdf.gz: gzip compressed data, was "4327_010.pdf", from Unix, last modified: Tue May  5 14:28:16 2015, max compression

但是md5不同:

$ md5sum 4327_010.pdf.gz ref_max/4327_010.pdf.gz 
39dc3e5a52c71a25c53fcbc02e2702d5  4327_010.pdf.gz
213a599a382cd887f3c4f963e1d3dec4  ref_max/4327_010.pdf.gz

gzip -l也不同:

$ gzip -l ref_max/4327_010.pdf.gz 4327_010.pdf.gz 
     compressed        uncompressed  ratio uncompressed_name
        7286404             7600522   4.1% ref_max/4327_010.pdf
        7297310             7600522   4.0% 4327_010.pdf

我猜这是因为 gzip 程序和 python gzip 模块(基于 C 库 zlib)的算法略有不同。

包装一个 gzip.GzipFile object around an io.BytesIO 对象。 (在Python 2中,使用cStringIO.StringIO代替。)关闭GzipFile后,您可以从BytesIO对象中检索压缩数据(使用getvalue) ,对其进行哈希处理,然后将其写入真实文件。

顺便说一下,你 really shouldn't be using MD5 at all anymore.

I have tried to use zlib.compress but the returned string miss the header of a gzip file.

当然可以。这就是 zlib module and the gzip 模块之间的全部区别; zlib 仅处理 zlib-deflate 压缩而不使用 gzip headers,gzip 处理 zlib-deflate 数据 with gzip headers.

因此,只需调用 gzip.compress,您编写但未向我们展示的代码应该可以正常工作。


旁注:

with open(filename) as f:
    md5 = hashlib.md5(f.read()).hexdigest()

您几乎肯定想在此处以 'rb' 模式打开文件。您不想将 '\r\n' 转换为 '\n'(如果在 Windows 上),或者将二进制数据解码为 sys.getdefaultencoding() 文本(如果在 Python 3 上) , 所以用二进制模式打开它。


另一个旁注:

不要在二进制文件上使用 line-based API。而不是这个:

f_out.writelines(f_in)

... 这样做:

f_out.write(f_in.read())

或者,如果文件太大而无法一次全部读入内存:

for buf in iter(partial(f_in.read, 8192), b''):
    f_out.write(buf)

最后一点:

With many files (> 10 000), each several MB when compressed, in a NFS mounted drive, it is slow.

您的系统没有安装在更快的驱动器上的 tmp 目录吗?

在大多数情况下,您不需要真实的文件。要么有 string-based API(zlib.compressgzip.compressjson.dumps 等),要么 file-based API 只需要一个 file-like object,就像一个 BytesIO.

但是当您确实需要一个真正的临时文件、一个真正的文件描述符和所有东西时,您几乎总是想在临时目录中创建它。 * 在 Python 中,您使用 tempfile 模块执行此操作。

例如:

def compress_and_md5(filename):
    with tempfile.NamedTemporaryFile() as f_out:
        with open(filename, 'rb') as f_in:
            g_out = gzip.open(f_out)
            g_out.write(f_in.read())
        f_out.seek(0)
        md5 = hashlib.md5(f_out.read()).hexdigest()

如果您需要一个实际的文件名,而不是一个文件 object,您可以使用 f_in.name.

* 一个例外是当您只希望临时文件最终 rename 到永久位置时。在那种情况下,当然,您通常希望临时文件与永久位置位于同一目录中。但是您可以使用 tempfile 轻松做到这一点。只记得传递 delete=False.