9

我对使用 Python 的gzip模块压缩数据感兴趣。碰巧我希望压缩输出是确定性的,因为这对于一般事物来说通常是一个非常方便的属性——例如,如果某个非 gzip 感知进程正在寻找输出中的变化,或者如果输出将被加密签名。

不幸的是,每次的输出都不一样。据我所知,唯一的原因是 gzip 标头中的时间戳字段,Python 模块始终使用当前时间填充该字段。我认为实际上不允许您使用没有时间戳的 gzip 流,这太糟糕了。

无论如何,Pythongzip模块的调用者似乎没有办法提供基础数据的正确修改时间。(实际gzip程序似乎尽可能使用输入文件的时间戳。)我想这是因为基本上唯一关心时间戳的是gunzip写入文件时的命令——现在,我,因为我想要确定性的输出。这是不是要求太过分了?

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

gzip对来自 Python 的任意时间戳的某些数据最不可怕的方法是什么?

4

7 回答 7

8

是的,你没有任何漂亮的选择。时间用 _write_gzip_header 中的这一行写入:

write32u(self.fileobj, long(time.time()))

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

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

这是选项#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)
于 2008-11-05T03:49:51.973 回答
6

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

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()
于 2016-03-16T11:25:36.173 回答
2

提交一个补丁,其中排除了时间戳的计算。它几乎肯定会被接受。

于 2008-11-05T15:17:54.923 回答
1

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

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

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

我可以模糊地想象尝试做一些棘手和复杂的事情,也许不是那么面向未来,以某种方式导入gzip模块的私有副本和猴子补丁但到那时,过滤器似乎更简单、更直接。

于 2008-11-06T21:17:09.707 回答
0

在 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() 时使用的可选时间参数,在这种情况下,我敢肯定如果您提交补丁,他们会喜欢的!

于 2008-11-05T03:44:19.397 回答
0

这不漂亮,但你可以用这样的东西临时修补 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

它不漂亮,但它可能会起作用。

于 2008-11-05T04:32:23.747 回答
0

类似于上面多米尼克的答案,但对于现有文件:

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
于 2017-05-18T18:06:32.567 回答