Question

Je suis intéressé par la compression de données à l'aide du module gzip de Python. Il se trouve que je veux que la sortie compressée soit déterministe, parce que c'est souvent une propriété très pratique pour avoir des choses en général - si un processus non conscient de gzip doit rechercher des changements dans la sortie, par exemple, ou si la sortie sera signée de manière cryptographique.

Malheureusement, le résultat est différent à chaque fois. Pour autant que je sache, la seule raison à cela est le champ timestamp de l'en-tête gzip, que le module Python remplit toujours avec l'heure actuelle. Je ne pense pas que vous soyez réellement autorisé à avoir un flux gzip sans horodatage, ce qui est dommage.

Dans tous les cas, l'appelant du module gzip de Python ne peut pas fournir l'heure de modification correcte des données sous-jacentes. (Le programme gzip semble utiliser l’horodatage du fichier d’entrée lorsque cela est possible.) J’imagine que c’est parce que la seule chose qui se soucie de l’horodatage est le gunzip . commande lors de l'écriture dans un fichier - et maintenant moi, parce que je veux une sortie déterministe. Est-ce tellement demander?

Quelqu'un at-il rencontré ce problème?

Quel est le moyen le moins terrible de gzip d’utiliser un horodatage arbitraire de Python?

Était-ce utile?

La solution

À partir de Python 2.7, vous pouvez spécifier l'heure à utiliser dans l'en-tête gzip. N.B. Le nom de fichier est également inclus dans l'en-tête et peut également être spécifié manuellement.

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()

Autres conseils

Oui, vous n'avez pas de jolies options. Le temps est écrit avec cette ligne dans _write_gzip_header:

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

Comme ils ne vous permettent pas de modifier l'heure, vous pouvez effectuer l'une des opérations suivantes:

  1. Dérivez une classe à partir de GzipFile et copiez la fonction _write_gzip_header dans votre classe dérivée, mais avec une valeur différente dans cette ligne.
  2. Après avoir importé le module gzip, attribuez un nouveau code à son membre horaire. Vous allez essentiellement fournir une nouvelle définition du nom heure dans le code gzip, de sorte que vous puissiez changer la signification de time.time ().
  3. Copiez l'intégralité du module gzip, nommez-le my_stable_gzip et modifiez la ligne requise.
  4. Transmettez un objet CStringIO en tant que fileobj et modifiez le flux secondaire une fois que gzip est terminé.
  5. Ecrivez un faux objet fichier qui garde la trace des octets écrits et transfère le tout dans un fichier réel, à l'exception des octets de l'horodatage que vous écrivez vous-même.

Voici un exemple de l'option n ° 2 (non testée):

class FakeTime:
    def time(self):
        return 1225856967.109

import gzip
gzip.time = FakeTime()

# Now call gzip, it will think time doesn't change!

L'option 5 est peut-être la plus propre en termes de ne pas dépendre des composants internes du module gzip (non testée):

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)

Soumettez un patch dans lequel le calcul de l'horodatage est factorisé. Cela serait presque certainement accepté.

J'ai suivi les conseils de M. Coventry et envoyé un correctif . Cependant, étant donné l'état actuel du calendrier de publication de Python, avec la version 3.0 à proximité, je ne m'attends pas à ce que cela apparaisse dans une version dans un avenir proche. Pourtant, nous verrons ce qui se passe!

Entre-temps, j'aime bien l'option 5 proposée par M. Batchelder consistant à canaliser le flux gzip via un petit filtre personnalisé qui définit correctement le champ d'horodatage. Cela ressemble à l'approche la plus propre. Comme il le démontre, le code requis est en fait assez petit, bien que son exemple dépende, pour une partie de sa simplicité, de l’hypothèse (actuellement valide) selon laquelle l’implémentation du module gzip choisira d’écrire l’horodatage avec exactement un appel de write () sur quatre octets. Néanmoins, je ne pense pas qu'il serait très difficile de proposer une version entièrement générale si nécessaire.

L’approche «monkey-patching» (option 2) est plutôt tentante pour sa simplicité mais me laisse faire une pause car j’écris une bibliothèque qui appelle gzip , et pas seulement un programme autonome, et il semble que pour moi que quelqu'un pourrait essayer d'appeler gzip depuis un autre thread avant que mon module ne soit prêt à inverser sa modification dans l'état global du module gzip . Ce serait particulièrement malheureux si l'autre thread essayait de tirer un coup semblable au singe! J'admets que ce problème potentiel ne semble pas très probable dans la pratique, mais imaginez combien il serait difficile de diagnostiquer un tel gâchis!

J'imagine vaguement essayer de faire quelque chose de délicat et de compliqué et peut-être moins évolutif que de pouvoir importer une copie privée du module gzip et de monkey-patch qui , mais à ce stade, un filtre semble plus simple et plus direct.

Dans lib / gzip.py, nous trouvons la méthode qui construit l'en-tête, y compris la partie contenant effectivement un horodatage. En Python 2.5, cela commence à la ligne 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')

Comme vous pouvez le constater, il utilise time.time () pour récupérer l’heure actuelle. Selon la documentation du module en ligne, time.time "renverra l'heure sous forme de nombre à virgule flottante exprimée en secondes depuis l'époque, en UTC". Ainsi, si vous changez cela en une constante à virgule flottante de votre choix, vous pouvez toujours avoir les mêmes en-têtes écrits. Je ne vois pas de meilleur moyen de faire cela à moins de vouloir en pirater un peu plus la bibliothèque pour accepter un paramètre de temps optionnel que vous utiliserez par défaut avec time.time () quand ce n'est pas spécifié, auquel cas, je suis sûr ils adoreraient si vous soumettiez un patch!

Ce n’est pas beau, mais vous pouvez monkeypatch time.time temporairement avec quelque chose comme ceci:

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

Ce n’est pas beau, mais ça marcherait probablement.

Semblable à la réponse de dominic ci-dessus, mais pour un fichier existant :

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)

Test des sommes MD5:

md5sum test_zip*
7e544bc6827232f67ff5508c8d6c30b3  test_zip1
75decc5768bdc3c98d6e598dea85e39b  test_zip1.gz
7e544bc6827232f67ff5508c8d6c30b3  test_zip2
75decc5768bdc3c98d6e598dea85e39b  test_zip2.gz
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top