2008-11-17 36 views
16

Tengo una gran cantidad de datos (un par de conciertos) Necesito escribir en un archivo zip en Python. No puedo cargar todo en la memoria de una vez para pasar al método .writestr de ZipFile, y realmente no quiero alimentarlo todo en el disco usando archivos temporales y luego volver a leerlo.¿Crear un archivo zip de un generador en Python?

¿Hay alguna manera de alimentar un generador o un objeto similar a un archivo a la biblioteca de ZipFile? ¿O hay alguna razón por la cual esta capacidad no parece ser compatible?

Por archivo zip, quiero decir archivo zip. Como se admite en el paquete de archivo zip de Python.

+1

Dije eso, tanto en el título como en la primera oración. He agregado una aclaración, aunque estoy desconcertado sobre por qué fue necesaria. Si solo necesitara algún algoritmo de compresión genérico, lo hubiera dicho en primer lugar. –

+0

Parece que ZIP significa GZIP para la mayor parte del mundo. Entonces, cuando te refieres a ZIP (como en PKWare ZIP), tienes que aclarar la distinción. Sí, es desconcertante por qué la gente cree en GZip cuando se refería a PKWare Zip. –

+0

Supongo que winzip, pkware zip y 7zip, que son las más conocidas '' gzip support '' de las aplicaciones de cremallera, podrían haber llevado a la gente a pensar que la implementación de gzip podría ser indolora. – hinoglu

Respuesta

10

La única solución es volver a escribir el método que utiliza para comprimir archivos para leer desde una memoria intermedia. Sería trivial agregar esto a las bibliotecas estándar; Estoy sorprendido de que todavía no se haya hecho. Supongo que hay un gran acuerdo sobre la necesidad de revisar toda la interfaz, y eso parece estar bloqueando cualquier mejora incremental.

import zipfile, zlib, binascii, struct 
class BufferedZipFile(zipfile.ZipFile): 
    def writebuffered(self, zipinfo, buffer): 
     zinfo = zipinfo 

     zinfo.file_size = file_size = 0 
     zinfo.flag_bits = 0x00 
     zinfo.header_offset = self.fp.tell() 

     self._writecheck(zinfo) 
     self._didModify = True 

     zinfo.CRC = CRC = 0 
     zinfo.compress_size = compress_size = 0 
     self.fp.write(zinfo.FileHeader()) 
     if zinfo.compress_type == zipfile.ZIP_DEFLATED: 
      cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) 
     else: 
      cmpr = None 

     while True: 
      buf = buffer.read(1024 * 8) 
      if not buf: 
       break 

      file_size = file_size + len(buf) 
      CRC = binascii.crc32(buf, CRC) & 0xffffffff 
      if cmpr: 
       buf = cmpr.compress(buf) 
       compress_size = compress_size + len(buf) 

      self.fp.write(buf) 

     if cmpr: 
      buf = cmpr.flush() 
      compress_size = compress_size + len(buf) 
      self.fp.write(buf) 
      zinfo.compress_size = compress_size 
     else: 
      zinfo.compress_size = file_size 

     zinfo.CRC = CRC 
     zinfo.file_size = file_size 

     position = self.fp.tell() 
     self.fp.seek(zinfo.header_offset + 14, 0) 
     self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size, zinfo.file_size)) 
     self.fp.seek(position, 0) 
     self.filelist.append(zinfo) 
     self.NameToInfo[zinfo.filename] = zinfo 
+0

Sí, la reescritura es un fastidio. Gracias a Dios por el código abierto. –

+0

Vea mi respuesta que lleva este enfoque un paso más allá: http://stackoverflow.com/questions/297345/create-a-zip-file-from-a-generator-in-python/2734156#2734156 – haridsv

+1

Hay un error en el código proporcionado; el argumento 'fmt' para' struck.pack' debe ser '"

3

gzip.GzipFile escribe los datos en trozos comprimidos, que pueden establecer el tamaño de los trozos de acuerdo con el número de líneas leídas de los archivos.

un ejemplo:

file = gzip.GzipFile('blah.gz', 'wb') 
sourcefile = open('source', 'rb') 
chunks = [] 
for line in sourcefile: 
    chunks.append(line) 
    if len(chunks) >= X: 
     file.write("".join(chunks)) 
     file.flush() 
     chunks = [] 
+0

Por razones desconocidas, el resultado debe ser un archivo ZIP, no un archivo GZIP o cualquier otra compresión. [Los comentarios eran insultantes, así que los borré]. –

+3

No especificado no es lo mismo que oscuro. Los archivos de archivo que produzco deben ser abiertos por los trabajadores de oficina en un entorno de Windows. Todos tienen servicios zip. Ninguno tiene GZIP. –

0

La biblioteca gzip tomará un objeto de fichero para la compresión.

class GzipFile([filename [,mode [,compresslevel [,fileobj]]]]) 

Todavía es necesario proporcionar un nombre de archivo nominal para su inclusión en el archivo zip, pero se pueden pasar los datos de código a la fileobj.

(Esta respuesta difiere de la de Damnsweet, en el que el foco debe estar en el origen de datos siendo incrementalmente leer, no el archivo comprimido que se forma incremental escrito.)

Y ahora ver el original interrogador no aceptará Gzip :-(

2

Algunos (muchos? la mayoría?) algoritmos de compresión se basan en mirar a través de las redundancias el archivo toda.

Algunos com Las bibliotecas de pression elegirán entre varios algoritmos de compresión en función de cuál funciona mejor en el archivo.

Creo que el módulo ZipFile hace esto, por lo que quiere ver todo el archivo, no solo piezas a la vez.

Por lo tanto, no funcionará con generadores o archivos para cargar en la memoria. Eso explicaría la limitación de la biblioteca Zipfile.

+1

+1 de acuerdo. No es imposible (ver la respuesta de Chris B.), pero creo que puede tener más sentido darle al algoritmo de compresión todo el asunto. El análisis, necesario para generar un buen árbol de codificación Huffman, es más preciso y con respecto al archivo completo. Por lo tanto, el resultado podría ser mucho más pequeño/mejor comprimido. –

3

La compresión esencial es realizada por zlib.compressobj. ZipFile (en Python 2.5 en MacOSX parece estar compilado). La versión de Python 2.3 es la siguiente.

Puede ver que crea el archivo comprimido en 8k trozos. La extracción de la información del archivo de origen es compleja porque muchos atributos del archivo de origen (como el tamaño sin comprimir) se registran en el encabezado del archivo zip.

def write(self, filename, arcname=None, compress_type=None): 
    """Put the bytes from filename into the archive under the name 
    arcname.""" 

    st = os.stat(filename) 
    mtime = time.localtime(st.st_mtime) 
    date_time = mtime[0:6] 
    # Create ZipInfo instance to store file information 
    if arcname is None: 
     zinfo = ZipInfo(filename, date_time) 
    else: 
     zinfo = ZipInfo(arcname, date_time) 
    zinfo.external_attr = st[0] << 16L  # Unix attributes 
    if compress_type is None: 
     zinfo.compress_type = self.compression 
    else: 
     zinfo.compress_type = compress_type 
    self._writecheck(zinfo) 
    fp = open(filename, "rb") 

    zinfo.flag_bits = 0x00 
    zinfo.header_offset = self.fp.tell() # Start of header bytes 
    # Must overwrite CRC and sizes with correct data later 
    zinfo.CRC = CRC = 0 
    zinfo.compress_size = compress_size = 0 
    zinfo.file_size = file_size = 0 
    self.fp.write(zinfo.FileHeader()) 
    zinfo.file_offset = self.fp.tell()  # Start of file bytes 
    if zinfo.compress_type == ZIP_DEFLATED: 
     cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, 
      zlib.DEFLATED, -15) 
    else: 
     cmpr = None 
    while 1: 
     buf = fp.read(1024 * 8) 
     if not buf: 
      break 
     file_size = file_size + len(buf) 
     CRC = binascii.crc32(buf, CRC) 
     if cmpr: 
      buf = cmpr.compress(buf) 
      compress_size = compress_size + len(buf) 
     self.fp.write(buf) 
    fp.close() 
    if cmpr: 
     buf = cmpr.flush() 
     compress_size = compress_size + len(buf) 
     self.fp.write(buf) 
     zinfo.compress_size = compress_size 
    else: 
     zinfo.compress_size = file_size 
    zinfo.CRC = CRC 
    zinfo.file_size = file_size 
    # Seek backwards and write CRC and file sizes 
    position = self.fp.tell()  # Preserve current position in file 
    self.fp.seek(zinfo.header_offset + 14, 0) 
    self.fp.write(struct.pack("<lLL", zinfo.CRC, zinfo.compress_size, 
      zinfo.file_size)) 
    self.fp.seek(position, 0) 
    self.filelist.append(zinfo) 
    self.NameToInfo[zinfo.filename] = zinfo 
+0

Sí, después de publicar la pregunta miré el código fuente. Tiene razón en que usa la información del archivo para el encabezado, pero no hay necesidad de hacerlo, de hecho, sobrescribe parte de la información de todos modos. He publicado una reescritura del método que hace lo que necesito. –

+0

Tenía la esperanza de evitar la reescritura, ya que depende de la parte interna de la biblioteca de Python para funcionar, pero realmente no parece haber otra forma. –

8

Tomé Chris B.'s answer y creé una solución completa.Aquí está, en caso de que alguien más está interesado:

import os 
import threading 
from zipfile import * 
import zlib, binascii, struct 

class ZipEntryWriter(threading.Thread): 
    def __init__(self, zf, zinfo, fileobj): 
     self.zf = zf 
     self.zinfo = zinfo 
     self.fileobj = fileobj 

     zinfo.file_size = 0 
     zinfo.flag_bits = 0x00 
     zinfo.header_offset = zf.fp.tell() 

     zf._writecheck(zinfo) 
     zf._didModify = True 

     zinfo.CRC = 0 
     zinfo.compress_size = compress_size = 0 
     zf.fp.write(zinfo.FileHeader()) 

     super(ZipEntryWriter, self).__init__() 

    def run(self): 
     zinfo = self.zinfo 
     zf = self.zf 
     file_size = 0 
     CRC = 0 

     if zinfo.compress_type == ZIP_DEFLATED: 
      cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) 
     else: 
      cmpr = None 
     while True: 
      buf = self.fileobj.read(1024 * 8) 
      if not buf: 
       self.fileobj.close() 
       break 

      file_size = file_size + len(buf) 
      CRC = binascii.crc32(buf, CRC) 
      if cmpr: 
       buf = cmpr.compress(buf) 
       compress_size = compress_size + len(buf) 

      zf.fp.write(buf) 

     if cmpr: 
      buf = cmpr.flush() 
      compress_size = compress_size + len(buf) 
      zf.fp.write(buf) 
      zinfo.compress_size = compress_size 
     else: 
      zinfo.compress_size = file_size 

     zinfo.CRC = CRC 
     zinfo.file_size = file_size 

     position = zf.fp.tell() 
     zf.fp.seek(zinfo.header_offset + 14, 0) 
     zf.fp.write(struct.pack("<lLL", zinfo.CRC, zinfo.compress_size, zinfo.file_size)) 
     zf.fp.seek(position, 0) 
     zf.filelist.append(zinfo) 
     zf.NameToInfo[zinfo.filename] = zinfo 

class EnhZipFile(ZipFile, object): 

    def _current_writer(self): 
     return hasattr(self, 'cur_writer') and self.cur_writer or None 

    def assert_no_current_writer(self): 
     cur_writer = self._current_writer() 
     if cur_writer and cur_writer.isAlive(): 
      raise ValueError('An entry is already started for name: %s' % cur_write.zinfo.filename) 

    def write(self, filename, arcname=None, compress_type=None): 
     self.assert_no_current_writer() 
     super(EnhZipFile, self).write(filename, arcname, compress_type) 

    def writestr(self, zinfo_or_arcname, bytes): 
     self.assert_no_current_writer() 
     super(EnhZipFile, self).writestr(zinfo_or_arcname, bytes) 

    def close(self): 
     self.finish_entry() 
     super(EnhZipFile, self).close() 

    def start_entry(self, zipinfo): 
     """ 
     Start writing a new entry with the specified ZipInfo and return a 
     file like object. Any data written to the file like object is 
     read by a background thread and written directly to the zip file. 
     Make sure to close the returned file object, before closing the 
     zipfile, or the close() would end up hanging indefinitely. 

     Only one entry can be open at any time. If multiple entries need to 
     be written, make sure to call finish_entry() before calling any of 
     these methods: 
     - start_entry 
     - write 
     - writestr 
     It is not necessary to explicitly call finish_entry() before closing 
     zipfile. 

     Example: 
      zf = EnhZipFile('tmp.zip', 'w') 
      w = zf.start_entry(ZipInfo('t.txt')) 
      w.write("some text") 
      w.close() 
      zf.close() 
     """ 
     self.assert_no_current_writer() 
     r, w = os.pipe() 
     self.cur_writer = ZipEntryWriter(self, zipinfo, os.fdopen(r, 'r')) 
     self.cur_writer.start() 
     return os.fdopen(w, 'w') 

    def finish_entry(self, timeout=None): 
     """ 
     Ensure that the ZipEntry that is currently being written is finished. 
     Joins on any background thread to exit. It is safe to call this method 
     multiple times. 
     """ 
     cur_writer = self._current_writer() 
     if not cur_writer or not cur_writer.isAlive(): 
      return 
     cur_writer.join(timeout) 

if __name__ == "__main__": 
    zf = EnhZipFile('c:/tmp/t.zip', 'w') 
    import time 
    w = zf.start_entry(ZipInfo('t.txt', time.localtime()[:6])) 
    w.write("Line1\n") 
    w.write("Line2\n") 
    w.close() 
    zf.finish_entry() 
    w = zf.start_entry(ZipInfo('p.txt', time.localtime()[:6])) 
    w.write("Some text\n") 
    w.close() 
    zf.close() 
+1

¿qué tal una versión sin rosca? los hilos en Python no agregarán rendimiento si eso es lo que pretendía. –

+0

Esto es agradable, pero cuando trato de usarlo como BytesIO (para la creación de documentos XML con XMLGenerator) me sale una excepción cuando intento y cierre el archivo. 'Traceback (llamada más reciente): Archivo" /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py ", línea 810, en __bootstrap_inner self.run () Archivo "bufzip.py", línea 62, en ejecución zf.fp.write (struct.pack ("

+0

Como Erik señaló anteriormente hay un error tipográfico en el formato de cadena que debe ser '"

0

Ésta es 2017. Si todavía buscando hacerlo elegantemente, use Python Zipstream by allanlei. Hasta ahora, es probablemente la única biblioteca bien escrita para lograr eso.

0

En caso de que alguien se tropiece con esta pregunta, que sigue siendo relevante en 2017 para Python 2.7, aquí hay una solución de trabajo para un archivo zip de transmisión real, sin necesidad de buscar la salida como en los demás casos. El secreto es establecer el bit 3 del indicador de bits de propósito general (ver https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT sección 4.3.9.1).

Tenga en cuenta que esta implementación siempre creará un archivo de estilo ZIP64, permitiendo que la transmisión funcione para archivos arbitrariamente grandes. Incluye un feo hack para forzar el final de zip64 del registro de directorio central, así que ten en cuenta que hará que todos los archivos comprimidos escritos por tu proceso se conviertan en estilo ZIP64.

import io 
import zipfile 
import zlib 
import binascii 
import struct 

class ByteStreamer(io.BytesIO): 
    ''' 
    Variant on BytesIO which lets you write and consume data while 
    keeping track of the total filesize written. When data is consumed 
    it is removed from memory, keeping the memory requirements low. 
    ''' 
    def __init__(self): 
     super(ByteStreamer, self).__init__() 
     self._tellall = 0 

    def tell(self): 
     return self._tellall 

    def write(self, b): 
     orig_size = super(ByteStreamer, self).tell() 
     super(ByteStreamer, self).write(b) 
     new_size = super(ByteStreamer, self).tell() 
     self._tellall += (new_size - orig_size) 

    def consume(self): 
     bytes = self.getvalue() 
     self.seek(0) 
     self.truncate(0) 
     return bytes 

class BufferedZipFileWriter(zipfile.ZipFile): 
    ''' 
    ZipFile writer with true streaming (input and output). 
    Created zip files are always ZIP64-style because it is the only safe way to stream 
    potentially large zip files without knowing the full size ahead of time. 

    Example usage: 
    >>> def stream(): 
    >>>  bzfw = BufferedZip64FileWriter() 
    >>>  for arc_path, buffer in inputs: # buffer is a file-like object which supports read(size) 
    >>>   for chunk in bzfw.streambuffer(arc_path, buffer): 
    >>>    yield chunk 
    >>>  yield bzfw.close() 
    ''' 
    def __init__(self, compression=zipfile.ZIP_DEFLATED): 
     self._buffer = ByteStreamer() 
     super(BufferedZipFileWriter, self).__init__(self._buffer, mode='w', compression=compression, allowZip64=True) 

    def streambuffer(self, zinfo_or_arcname, buffer, chunksize=2**16): 
     if not isinstance(zinfo_or_arcname, zipfile.ZipInfo): 
      zinfo = zipfile.ZipInfo(filename=zinfo_or_arcname, 
            date_time=time.localtime(time.time())[:6]) 
      zinfo.compress_type = self.compression 
      zinfo.external_attr = 0o600 << 16  # ?rw------- 
     else: 
      zinfo = zinfo_or_arcname 

     zinfo.file_size = file_size = 0 
     zinfo.flag_bits = 0x08 # Streaming mode: crc and size come after the data 
     zinfo.header_offset = self.fp.tell() 

     self._writecheck(zinfo) 
     self._didModify = True 

     zinfo.CRC = CRC = 0 
     zinfo.compress_size = compress_size = 0 
     self.fp.write(zinfo.FileHeader()) 
     if zinfo.compress_type == zipfile.ZIP_DEFLATED: 
      cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) 
     else: 
      cmpr = None 

     while True: 
      buf = buffer.read(chunksize) 
      if not buf: 
       break 

      file_size += len(buf) 
      CRC = binascii.crc32(buf, CRC) & 0xffffffff 
      if cmpr: 
       buf = cmpr.compress(buf) 
       compress_size += len(buf) 

      self.fp.write(buf) 
      compressed_bytes = self._buffer.consume() 
      if compressed_bytes: 
       yield compressed_bytes 

     if cmpr: 
      buf = cmpr.flush() 
      compress_size += len(buf) 
      self.fp.write(buf) 
      zinfo.compress_size = compress_size 
      compressed_bytes = self._buffer.consume() 
      if compressed_bytes: 
       yield compressed_bytes 
     else: 
      zinfo.compress_size = file_size 

     zinfo.CRC = CRC 
     zinfo.file_size = file_size 

     # Write CRC and file sizes after the file data 
     # Always write as zip64 -- only safe way to stream what might become a large zipfile 
     fmt = '<LQQ' 
     self.fp.write(struct.pack(fmt, zinfo.CRC, zinfo.compress_size, zinfo.file_size)) 

     self.fp.flush() 
     self.filelist.append(zinfo) 
     self.NameToInfo[zinfo.filename] = zinfo 
     yield self._buffer.consume() 

    # The close method needs to be patched to force writing a ZIP64 file 
    # We'll hack ZIP_FILECOUNT_LIMIT to do the forcing 
    def close(self): 
     tmp = zipfile.ZIP_FILECOUNT_LIMIT 
     zipfile.ZIP_FILECOUNT_LIMIT = 0 
     super(BufferedZipFileWriter, self).close() 
     zipfile.ZIP_FILECOUNT_LIMIT = tmp 
     return self._buffer.consume() 
Cuestiones relacionadas