Domanda

Sono interessato a comprimere i dati usando il modulo gzip di Python. Succede che voglio che l'output compresso sia deterministico, perché questa è spesso una proprietà davvero conveniente per le cose in generale - se qualche processo non compatibile con gzip cercherà cambiamenti nell'output, diciamo, o se l'uscita verrà firmata crittograficamente.

Sfortunatamente, l'output è diverso ogni volta. Per quanto ne so, l'unica ragione di ciò è il campo timestamp nell'intestazione gzip, che il modulo Python popola sempre con l'ora corrente. Non penso che ti sia effettivamente permesso di avere un flusso gzip senza un timestamp, il che è troppo male.

In ogni caso, non sembra esserci un modo per il chiamante del modulo gzip di Python di fornire il tempo di modifica corretto dei dati sottostanti. (L'attuale programma gzip sembra usare il timestamp del file di input quando possibile.) Immagino che ciò sia dovuto al fatto che l'unica cosa a cui importa del timestamp è il gunzip comando quando scrivo su un file - e, ora, io, perché voglio un output deterministico. È così tanto da chiedere?

Qualcun altro ha riscontrato questo problema?

Qual è il modo meno terribile per gzip alcuni dati con un timestamp arbitrario da Python?

È stato utile?

Soluzione

Da Python 2.7 in poi puoi specificare il tempo da usare nell'intestazione gzip. N.B. il nome file è anche incluso nell'intestazione e può anche essere specificato manualmente.

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

Altri suggerimenti

Sì, non hai opzioni carine. Il tempo è scritto con questa riga in _write_gzip_header:

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

Dato che non ti danno modo di sovrascrivere il tempo, puoi fare una di queste cose:

  1. Deriva una classe da GzipFile e copia la funzione _write_gzip_header nella tua classe derivata, ma con un valore diverso in questa riga.
  2. Dopo aver importato il modulo gzip, assegnare un nuovo codice al suo membro temporale. In sostanza fornirai una nuova definizione dell'ora del nome nel codice gzip, in modo da poter cambiare il significato di time.time ().
  3. Copia l'intero modulo gzip, chiamalo my_stable_gzip e cambia la riga che devi.
  4. Passa un oggetto CStringIO come fileobj e modifica il bytestream al termine di gzip.
  5. Scrivi un oggetto file falso che tiene traccia dei byte scritti e passa tutto in un file reale, tranne i byte per il timestamp, che scrivi tu stesso.

Ecco un esempio dell'opzione # 2 (non testato):

class FakeTime:
    def time(self):
        return 1225856967.109

import gzip
gzip.time = FakeTime()

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

L'opzione n. 5 potrebbe essere la più pulita in termini di non dipendere dagli interni del modulo gzip (non testato):

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)

Invia una patch in cui viene calcolato il calcolo del timestamp. Quasi certamente sarebbe accettato.

Ho seguito il consiglio di Mr. Coventry e ha inviato una patch . Tuttavia, dato lo stato attuale della pianificazione delle versioni di Python, con la versione 3.0 proprio dietro l'angolo, non mi aspetto che venga presto visualizzata in una versione. Tuttavia, vedremo cosa succede!

Nel frattempo, mi piace l'opzione 5 di Mr. Batchelder di eseguire il piping del flusso gzip attraverso un piccolo filtro personalizzato che imposta correttamente il campo data / ora. Sembra l'approccio più pulito. Come dimostra, il codice richiesto è in realtà piuttosto piccolo, sebbene il suo esempio dipenda per una parte della sua semplicità dal presupposto (attualmente valido) che l'implementazione del modulo gzip sceglierà di scrivere il timestamp usando esattamente uno chiamata a quattro byte a write () . Tuttavia, non penso che sarebbe molto difficile trovare una versione completamente generale, se necessario.

L'approccio del monkey-patching (aka opzione 2) è abbastanza allettante per la sua semplicità ma mi fa una pausa perché sto scrivendo una libreria che chiama gzip , non solo un programma autonomo, e sembra per me qualcuno potrebbe provare a chiamare gzip da un altro thread prima che il mio modulo sia pronto per invertire la sua modifica allo stato globale del modulo gzip . Ciò sarebbe particolarmente sfortunato se l'altro thread stesse provando a fare una simile acrobazia con patch scimmia! Ammetto che questo potenziale problema non sembra molto probabile che si verifichi in pratica, ma immagina quanto sarebbe doloroso diagnosticare un tale casino!

Posso vagamente immaginare di provare a fare qualcosa di complicato e complicato e forse non così a prova di futuro per importare in qualche modo una copia privata del modulo gzip e scimmia patch che , ma a quel punto un filtro sembra più semplice e diretto.

In lib / gzip.py, troviamo il metodo che costruisce l'intestazione, inclusa la parte che effettivamente contiene un timestamp. In Python 2.5, questo inizia alla riga 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')

Come puoi vedere, usa time.time () per recuperare l'ora corrente. Secondo i documenti del modulo online, time.time restituirà il tempo come un numero in virgola mobile espresso in secondi dall'epoca, in UTC. & Quot; Quindi, se lo cambi in una costante a virgola mobile di tua scelta, puoi sempre avere le stesse intestazioni scritte. Non riesco a vedere un modo migliore per farlo a meno che tu non voglia hackerare un po 'di più la libreria per accettare un parametro di tempo opzionale che usi mentre passi a time.time () quando non è specificato, nel qual caso, sono sicuro lo adorerebbero se inviassi una patch!

Non è carino, ma potresti sincronizzare time.time temporaneamente con qualcosa del genere:

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

Non è carino, ma probabilmente funzionerebbe.

Simile alla risposta di Dominic sopra, ma per un file esistente:

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 delle somme MD5:

md5sum test_zip*
7e544bc6827232f67ff5508c8d6c30b3  test_zip1
75decc5768bdc3c98d6e598dea85e39b  test_zip1.gz
7e544bc6827232f67ff5508c8d6c30b3  test_zip2
75decc5768bdc3c98d6e598dea85e39b  test_zip2.gz
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top