我对使用Python的 gzip 模块压缩数据感兴趣。碰巧我希望压缩输出是确定性的,因为通常情况下这通常是一个非常方便的属性 - 如果一些非gzip感知的进程要在输出中寻找变化,比如说,或者如果输出将以加密方式签名。

不幸的是,每次输出都不同。据我所知,唯一的原因是gzip头中的timestamp字段,Python模块总是填充当前时间。我认为你实际上不允许你有一个没有时间戳的gzip流,这太糟糕了。

在任何情况下,Python的 gzip 模块的调用者似乎都没有办法提供底层数据的正确修改时间。 (实际的 gzip 程序似乎尽可能使用输入文件的时间戳。)我想这是因为基本上唯一关心时间戳的是 gunzip 写入文件时的命令 - 现在,我,因为我想要确定性输出。有这么多要问吗?

还有其他人遇到过这个问题吗?

使用Python的任意时间戳 gzip 一些最不可靠的方法是什么?

有帮助吗?

解决方案

从Python 2.7开始,您可以指定在gzip头中使用的时间。注:文件名也包含在标题中,也可以手动指定。

import gzip

content = b"Some content"
f = open("/tmp/f.gz", "wb")
gz = gzip.GzipFile(fileobj=f,mode="wb",filename="",mtime=0)
gz.write(content)
gz.close()
f.close()

其他提示

是的,你没有任何漂亮的选择。在_write_gzip_header:

中使用此行写入时间
write32u(self.fileobj, long(time.time()))

由于他们没有给你一种覆盖时间的方法,你可以做以下事情之一:

  1. 从GzipFile派生一个类,并将 _write_gzip_header 函数复制到派生类中,但在这一行中使用不同的值。
  2. 导入gzip模块后,将新代码分配给其时间成员。您将基本上在gzip代码中提供名称时间的新定义,因此您可以更改time.time()的含义。
  3. 复制整个gzip模块,并将其命名为my_stable_gzip,然后更改所需的行。
  4. 将CStringIO对象作为fileobj传递,并在完成gzip后修改字节流。
  5. 写一个伪文件对象来跟踪写入的字节,并将所有内容传递给一个真实的文件,除了您自己编写的时间戳的字节数。
  6. 以下是选项#2(未经测试)的示例:

    class FakeTime:
        def time(self):
            return 1225856967.109
    
    import gzip
    gzip.time = FakeTime()
    
    # Now call gzip, it will think time doesn't change!
    

    选项#5可能是最简洁的,不依赖于gzip模块的内部结构(未经测试):

    class GzipTimeFixingFile:
        def __init__(self, realfile):
            self.realfile = realfile
            self.pos = 0
    
        def write(self, bytes):
            if self.pos == 4 and len(bytes) == 4:
                self.realfile.write("XYZY")  # Fake time goes here.
            else:
                self.realfile.write(bytes)
            self.pos += len(bytes)
    

提交补丁,其中计算时间戳已计算在内。几乎肯定会被接受。

我接受了考文垂先生的建议并提交了补丁。但是,考虑到Python发布计划的当前状态,即将推出3.0,我不希望它很快就会出现在发布版本中。不过,我们会看到会发生什么!

与此同时,我喜欢Batchelder先生的选项5,即通过一个小的自定义过滤器来管理gzip流,该过滤器可以正确设置时间戳字段。这听起来像是最干净的方法。正如他演示的那样,所需的代码实际上非常小,尽管他的示例确实依赖于(当前有效的)假设的一些简单性,即 gzip 模块实现将选择仅使用一个写入时间戳对 write()的四字节调用。不过,如果需要的话,我认为想出一个完全通用的版本并不是很困难。

猴子修补方法(又称选项2)非常诱人,因为它的简单性让我暂停,因为我正在编写一个调用 gzip 的库,而不仅仅是一个独立的程序,似乎对我来说,在我的模块准备好将其更改转换为 gzip 模块的全局状态之前,有人可能会尝试从另一个线程调用 gzip 。如果另一个线程试图拉出类似的猴子修补特技,这将是特别不幸的!我承认这个潜在的问题听起来不太可能在实践中出现,但想象一下诊断这样一团糟是多么痛苦!

我可以模糊地想象尝试做一些棘手和复杂的事情,也许不会以某种方式导入 gzip 模块和monkey-patch 的私有副本 ,但到那时,过滤器似乎更简单,更直接。

在lib / gzip.py中,我们找到构建头的方法,包括确实包含时间戳的部分。在Python 2.5中,这从第143行开始:

def _write_gzip_header(self):
    self.fileobj.write('\037\213')             # magic header
    self.fileobj.write('\010')                 # compression method
    fname = self.filename[:-3]
    flags = 0
    if fname:
        flags = FNAME
    self.fileobj.write(chr(flags))
    write32u(self.fileobj, long(time.time())) # The current time!
    self.fileobj.write('\002')
    self.fileobj.write('\377')
    if fname:
        self.fileobj.write(fname + '\000')

如您所见,它使用time.time()来获取当前时间。根据在线模块文档,time.time将“返回时间作为以纪元为单位以秒表示的浮点数,以UTC表示。”因此,如果将此更改为您选择的浮点常量,则始终可以写出相同的标题。我无法看到更好的方法来执行此操作,除非您想要更多地破解库以接受您使用的可选时间参数,而默认为time.time()时未指定,在这种情况下,我确定如果你提交补丁,他们会喜欢它!

它不漂亮,但你可以用这样的东西暂时monkeypatch time.time:

import time

def fake_time():
  return 100000000.0

def do_gzip(content):
    orig_time = time.time
    time.time = fake_time
    # result = do gzip stuff here
    time.time = orig_time
    return result

它不漂亮,但可能会有效。

与上面的dominic的答案类似,但对于现有的文件:

with open('test_zip1', 'rb') as f_in, open('test_zip1.gz', 'wb') as f_out:
    with gzip.GzipFile(fileobj=f_out, mode='wb', filename="", mtime=0) as gz_out:
         shutil.copyfileobj(f_in, gz_out)

测试MD5总和:

md5sum test_zip*
7e544bc6827232f67ff5508c8d6c30b3  test_zip1
75decc5768bdc3c98d6e598dea85e39b  test_zip1.gz
7e544bc6827232f67ff5508c8d6c30b3  test_zip2
75decc5768bdc3c98d6e598dea85e39b  test_zip2.gz
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top