установка временной метки gzip из Python

Меня интересует сжатие данных с помощью модуля Python gzip. Бывает, что я хочу, чтобы сжатый вывод был детерминированным, потому что это часто очень удобное свойство для вещей в целом — если какой-то процесс, не поддерживающий gzip, будет искать изменения в выводе, скажем, или если вывод будет криптографически подписан.

К сожалению, результат каждый раз разный. Насколько я могу судить, единственной причиной этого является поле метки времени в заголовке gzip, которое модуль Python всегда заполняет текущим временем. Я не думаю, что вам действительно разрешено иметь поток gzip без временной метки, что очень плохо.

В любом случае у вызывающей стороны модуля Python gzip, по-видимому, нет способа указать правильное время модификации базовых данных. (Похоже, что реальная программа gzip использует временную метку входного файла, когда это возможно.) Я полагаю, это потому, что в основном единственное, что когда-либо заботится о временной метке, — это команда gunzip при записи в файл — и, теперь, я, потому что я хочу детерминированный вывод. Это так много, чтобы спросить?

Кто-нибудь еще сталкивался с этой проблемой?

Каков наименее ужасный способ gzip некоторых данных с произвольной отметкой времени из Python?


person zaphod    schedule 05.11.2008    source источник


Ответы (7)


Начиная с 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()
person Dominic Bevacqua    schedule 16.03.2016

Да, у тебя нет красивых вариантов. Время записывается этой строкой в ​​_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. Напишите поддельный файловый объект, который отслеживает записанные байты и передает все в настоящий файл, кроме байтов для временной метки, которую вы пишете сами.

Вот пример варианта № 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)
person Ned Batchelder    schedule 05.11.2008
comment
Решение № 2 работает для меня. Как был выбран 1225856967.109? Это та же временная метка, что и байты, которые gzip --no-name или mtime=0 python вернут? Было бы неплохо, если бы все реализации для переопределения времени использовали одно и то же значение. - person Daniel Himmelstein; 01.08.2019

Отправьте заплату, в которой не учитывается вычисление метки времени. Его почти наверняка примут.

person Alex Coventry    schedule 05.11.2008
comment
Я не могу представить, что патч появится в Ubuntu (которую я использую) в течение достаточно долгого времени, а это значит, что мне все еще нужен обходной путь. Тем не менее, я думаю, что это отличный ответ! - person zaphod; 06.11.2008

Я последовал совету г-на Ковентри и отправил исправление. Однако, учитывая текущее состояние графика выпуска Python, когда 3.0 не за горами, я не ожидаю, что он появится в выпуске в ближайшее время. Тем не менее, посмотрим, что будет!

А пока мне нравится вариант 5 г-на Бэтчелдера, заключающийся в передаче потока gzip через небольшой настраиваемый фильтр, который правильно устанавливает поле метки времени. Это звучит как самый чистый подход. Как он показывает, требуемый код на самом деле довольно мал, хотя его пример в некоторой степени зависит от своей простоты на предположении (действительно в настоящее время) о том, что реализация модуля gzip выберет запись временной метки, используя ровно один четырехбайтовый вызов write(). Тем не менее, я не думаю, что было бы очень сложно придумать полностью общую версию, если это необходимо.

Подход с исправлением обезьяны (также известный как вариант 2) довольно заманчив своей простотой, но заставляет меня задуматься, потому что я пишу библиотеку, которая вызывает gzip, а не просто отдельную программу, и мне кажется, что кто-то может попытаться вызвать gzip из другой поток, прежде чем мой модуль будет готов обратить свое изменение в глобальное состояние модуля gzip. Это было бы особенно прискорбно, если бы другой поток пытался провернуть аналогичный трюк с исправлением обезьяны! Я признаю, что эта потенциальная проблема вряд ли возникнет на практике, но представьте, как болезненно было бы диагностировать такой беспорядок!

Я смутно представляю себе попытку сделать что-то хитрое и сложное и, возможно, не настолько перспективное, чтобы каким-то образом импортировать частную копию модуля gzip и это с помощью обезьяны-патча, но к этому моменту фильтр кажется более простым и более прямой.

person zaphod    schedule 06.11.2008

В 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(), когда он не указан, и в этом случае я уверен им будет приятно, если вы представите патч!

person Sean    schedule 05.11.2008

Это некрасиво, но вы могли бы временно изменить 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

Это некрасиво, но, вероятно, сработает.

person Tony Arkles    schedule 05.11.2008
comment
Мое главное возражение против этого подхода заключается в том, что я пишу библиотеку и что вызывающая сторона моей библиотеки может пытаться использовать gzip в другом потоке, и в этом случае вносимые мной изменения потенциально могут повлиять на другие потоки. Это особенно ужасно, если другие потоки пытаются использовать тот же трюк! - person zaphod; 06.11.2008

Аналогично ответу Доминика выше, но для существующего файла:

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
person storm_m2138    schedule 18.05.2017