impostazione del timestamp gzip da Python
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?
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:
- Deriva una classe da GzipFile e copia la funzione
_write_gzip_header
nella tua classe derivata, ma con un valore diverso in questa riga. - 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 ().
- Copia l'intero modulo gzip, chiamalo my_stable_gzip e cambia la riga che devi.
- Passa un oggetto CStringIO come fileobj e modifica il bytestream al termine di gzip.
- 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