zlib — Compresión de GNU zlib

Propósito:Acceso de bajo nivel a la biblioteca de compresión GNU zlib

El módulo zlib proporciona una interfaz de nivel inferior para muchos de las funciones en la biblioteca de compresión zlib del proyecto GNU.

Trabajar con datos en memoria

La forma más sencilla de trabajar con zlib requiere mantener todos los datos a comprimir o descomprimir en la memoria.

zlib_memory.py
import zlib
import binascii

original_data = b'This is the original text.'
print('Original     :', len(original_data), original_data)

compressed = zlib.compress(original_data)
print('Compressed   :', len(compressed),
      binascii.hexlify(compressed))

decompressed = zlib.decompress(compressed)
print('Decompressed :', len(decompressed), decompressed)

Las funciones compress() y decompress() ambas toman un argumento de secuencia de bytes y devuelven una secuencia de bytes.

$ python3 zlib_memory.py

Original     : 26 b'This is the original text.'
Compressed   : 32 b'789c0bc9c82c5600a2928c5485fca2ccf4ccbcc41c85
92d48a123d007f2f097e'
Decompressed : 26 b'This is the original text.'

El ejemplo anterior demuestra que la versión comprimida de pequeñas cantidades de datos pueden ser mayores que la versión sin comprimir. Mientras que los resultados reales dependen de los datos de entrada, es interesante observar la sobrecarga de compresión para pequeños conjuntos de datos.

zlib_lengths.py
import zlib

original_data = b'This is the original text.'

template = '{:>15}  {:>15}'
print(template.format('len(data)', 'len(compressed)'))
print(template.format('-' * 15, '-' * 15))

for i in range(5):
    data = original_data * i
    compressed = zlib.compress(data)
    highlight = '*' if len(data) < len(compressed) else ''
    print(template.format(len(data), len(compressed)), highlight)

El * en la salida resalta las líneas donde los datos comprimidos ocupan más memoria que la versión sin comprimir.

$ python3 zlib_lengths.py

      len(data)  len(compressed)
---------------  ---------------
              0                8 *
             26               32 *
             52               35
             78               35
            104               36

zlib soporta varios niveles de compresión diferentes, permitiendo un equilibro entre el costo computacional y la cantidad de reducción de espacio. El nivel de compresión por defecto, zlib.Z_DEFAULT_COMPRESSION es -1 y corresponde a un valor codificado que hace un compromiso entre el rendimiento y el resultado de la compresión. Esto actualmente corresponde al nivel 6.

zlib_compresslevel.py
import zlib

input_data = b'Some repeated text.\n' * 1024
template = '{:>5}  {:>5}'

print(template.format('Level', 'Size'))
print(template.format('-----', '----'))

for i in range(0, 10):
    data = zlib.compress(input_data, i)
    print(template.format(i, len(data)))

Un nivel de 0 significa que no hay compresión en absoluto. Un nivel de 9 requiere la mayoría de los cálculos y produce la salida más pequeña. Como este ejemplo muestra, la misma reducción de tamaño se puede lograr con múltiples niveles de compresión para una entrada determinada.

$ python3 zlib_compresslevel.py

Level   Size
-----   ----
    0  20491
    1    172
    2    172
    3    172
    4     98
    5     98
    6     98
    7     98
    8     98
    9     98

Compresión y descompresión incremental

El enfoque en memoria tiene inconvenientes que lo hacen poco práctico para casos de uso del mundo real, principalmente que el sistema necesita suficiente memoria para mantener ambas versiones sin comprimir y comprimidas residentes en la memoria al mismo tiempo. La alternativa es usar Compress y Decompress para manipular los datos de manera incremental, de modo que no es necesario que todo el conjunto de datos encaje en la memoria.

zlib_incremental.py
import zlib
import binascii

compressor = zlib.compressobj(1)

with open('lorem.txt', 'rb') as input:
    while True:
        block = input.read(64)
        if not block:
            break
        compressed = compressor.compress(block)
        if compressed:
            print('Compressed: {}'.format(
                binascii.hexlify(compressed)))
        else:
            print('buffering...')
    remaining = compressor.flush()
    print('Flushed: {}'.format(binascii.hexlify(remaining)))

Este ejemplo lee pequeños bloques de datos de un archivo de texto plano y se los pasa a compress(). El compresor mantiene un buffer interno de datos comprimidos. Dado que el algoritmo de compresión depende de sumas de comprobación y tamaños de bloque mínimos, el compresor puede no estar listo para devolver datos cada vez que recibe más entrada. Si no tiene listo todo el bloque comprimido, devuelve una cadena de bytes vacía. Cuando todos los datos se alimentan, el método flush() obliga al compresor para cerrar el bloque final y devuelve el resto de la datos comprimidos.

$ python3 zlib_incremental.py

Compressed: b'7801'
buffering...
buffering...
buffering...
buffering...
buffering...
Flushed: b'55904b6ac4400c44f73e451da0f129b20c2110c85e696b8c40dde
dd167ce1f7915025a087daa9ef4be8c07e4f21c38962e834b800647435fd3b90
747b2810eb9c4bbcc13ac123bded6e4bef1c91ee40d3c6580e3ff52aad2e8cb2
eb6062dad74a89ca904cbb0f2545e0db4b1f2e01955b8c511cb2ac08967d228a
f1447c8ec72e40c4c714116e60cdef171bb6c0feaa255dff1c507c2c4439ec96
05b7e0ba9fc54bae39355cb89fd6ebe5841d673c7b7bc68a46f575a312eebd22
0d4b32441bdc1b36ebf0aedef3d57ea4b26dd986dd39af57dfb05d32279de'

Flujos de contenido mixto

La clase Decompress devuelta por decompressobj() también se puede utilizar en situaciones donde los datos comprimidos y sin comprimir son mezclados.

zlib_mixed.py
import zlib

lorem = open('lorem.txt', 'rb').read()
compressed = zlib.compress(lorem)
combined = compressed + lorem

decompressor = zlib.decompressobj()
decompressed = decompressor.decompress(combined)

decompressed_matches = decompressed == lorem
print('Decompressed matches lorem:', decompressed_matches)

unused_matches = decompressor.unused_data == lorem
print('Unused data matches lorem :', unused_matches)

Después de descomprimir todos los datos, el atributo unused_data contiene cualquier dato no utilizado.

$ python3 zlib_mixed.py

Decompressed matches lorem: True
Unused data matches lorem : True

Sumas de control

Además de las funciones de compresión y descompresión, zlib incluye dos funciones para calcular sumas de comprobación de datos, adler32() y crc32(). Ninguna de las sumas de control es criptográficamente segura, y solo están destinadas a ser utilizadas para la verificación de la integridad datos.

zlib_checksums.py
import zlib

data = open('lorem.txt', 'rb').read()

cksum = zlib.adler32(data)
print('Adler32: {:12d}'.format(cksum))
print('       : {:12d}'.format(zlib.adler32(data, cksum)))

cksum = zlib.crc32(data)
print('CRC-32 : {:12d}'.format(cksum))
print('       : {:12d}'.format(zlib.crc32(data, cksum)))

Ambas funciones toman los mismos argumentos, una cadena de bytes que contiene los datos y un valor opcional que se utilizará como punto de partida para la suma de control. Devuelven un valor entero con signo de 32 bits que también puede ser pasado a las llamadas subsiguientes como un nuevo argumento de punto de partida para producir una suma de comprobación en directo.

$ python3 zlib_checksums.py

Adler32:   3542251998
       :    669447099
CRC-32 :   3038370516
       :   2870078631

Comprimir datos de red

El servidor en la siguiente lista usa el compresor de flujo para responder a las solicitudes que consisten en nombres de archivos escribiendo una versión comprimida del archivo al socket utilizado para comunicarse con el cliente.

zlib_server.py
import zlib
import logging
import socketserver
import binascii

BLOCK_SIZE = 64


class ZlibRequestHandler(socketserver.BaseRequestHandler):

    logger = logging.getLogger('Server')

    def handle(self):
        compressor = zlib.compressobj(1)

        # Find out what file the client wants
        filename = self.request.recv(1024).decode('utf-8')
        self.logger.debug('client asked for: %r', filename)

        # Send chunks of the file as they are compressed
        with open(filename, 'rb') as input:
            while True:
                block = input.read(BLOCK_SIZE)
                if not block:
                    break
                self.logger.debug('RAW %r', block)
                compressed = compressor.compress(block)
                if compressed:
                    self.logger.debug(
                        'SENDING %r',
                        binascii.hexlify(compressed))
                    self.request.send(compressed)
                else:
                    self.logger.debug('BUFFERING')

        # Send any data being buffered by the compressor
        remaining = compressor.flush()
        while remaining:
            to_send = remaining[:BLOCK_SIZE]
            remaining = remaining[BLOCK_SIZE:]
            self.logger.debug('FLUSHING %r',
                              binascii.hexlify(to_send))
            self.request.send(to_send)
        return


if __name__ == '__main__':
    import socket
    import threading
    from io import BytesIO

    logging.basicConfig(
        level=logging.DEBUG,
        format='%(name)s: %(message)s',
    )
    logger = logging.getLogger('Client')

    # Set up a server, running in a separate thread
    address = ('localhost', 0)  # let the kernel assign a port
    server = socketserver.TCPServer(address, ZlibRequestHandler)
    ip, port = server.server_address  # what port was assigned?

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)
    t.start()

    # Connect to the server as a client
    logger.info('Contacting server on %s:%s', ip, port)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Ask for a file
    requested_file = 'lorem.txt'
    logger.debug('sending filename: %r', requested_file)
    len_sent = s.send(requested_file.encode('utf-8'))

    # Receive a response
    buffer = BytesIO()
    decompressor = zlib.decompressobj()
    while True:
        response = s.recv(BLOCK_SIZE)
        if not response:
            break
        logger.debug('READ %r', binascii.hexlify(response))

        # Include any unconsumed data when
        # feeding the decompressor.
        to_decompress = decompressor.unconsumed_tail + response
        while to_decompress:
            decompressed = decompressor.decompress(to_decompress)
            if decompressed:
                logger.debug('DECOMPRESSED %r', decompressed)
                buffer.write(decompressed)
                # Look for unconsumed data due to buffer overflow
                to_decompress = decompressor.unconsumed_tail
            else:
                logger.debug('BUFFERING')
                to_decompress = None

    # deal with data reamining inside the decompressor buffer
    remainder = decompressor.flush()
    if remainder:
        logger.debug('FLUSHED %r', remainder)
        buffer.write(remainder)

    full_response = buffer.getvalue()
    lorem = open('lorem.txt', 'rb').read()
    logger.debug('response matches file contents: %s',
                 full_response == lorem)

    # Clean up
    s.close()
    server.socket.close()

Tiene algunos fragmentos artificiales en su lugar para ilustrar el comportamiento de almacenamiento en bufer que ocurre cuando los datos que se pasan a compress() o decompress() no produce un bloque completo de salida comprimido o salida sin comprimir.

El cliente se conecta al socket y solicita un archivo. Luego itera, recibiendo bloques de datos comprimidos. Dado que un bloque no puede contener suficiente información para descomprimirlo por completo, el resto de cualquiera de los datos recibidos anteriormente se combinan con los nuevos datos y se pasan al descompresor. A medida que los datos se descomprimen, se adjuntan a un bufer, que se compara con el contenido del archivo al final del bucle de procesamiento.

Advertencia

This server has obvious security implications. Do not run it on a system on the open Internet or in any environment where security might be an issue.

$ python3 zlib_server.py

Client: Contacting server on 127.0.0.1:53658
Client: sending filename: 'lorem.txt'
Server: client asked for: 'lorem.txt'
Server: RAW b'Lorem ipsum dolor sit amet, consectetuer adipiscin
g elit. Donec\n'
Server: SENDING b'7801'
Server: RAW b'egestas, enim et consectetuer ullamcorper, lectus
ligula rutrum '
Server: BUFFERING
Server: RAW b'leo, a\nelementum elit tortor eu quam. Duis tincid
unt nisi ut ant'
Server: BUFFERING
Server: RAW b'e. Nulla\nfacilisi. Sed tristique eros eu libero.
Pellentesque ve'
Server: BUFFERING
Server: RAW b'l arcu. Vivamus\npurus orci, iaculis ac, suscipit
sit amet, pulvi'
Client: READ b'7801'
Client: BUFFERING
Server: BUFFERING
Server: RAW b'nar eu,\nlacus.\n'
Server: BUFFERING
Server: FLUSHING b'55904b6ac4400c44f73e451da0f129b20c2110c85e696
b8c40ddedd167ce1f7915025a087daa9ef4be8c07e4f21c38962e834b8006474
35fd3b90747b2810eb9'
Server: FLUSHING b'c4bbcc13ac123bded6e4bef1c91ee40d3c6580e3ff52a
ad2e8cb2eb6062dad74a89ca904cbb0f2545e0db4b1f2e01955b8c511cb2ac08
967d228af1447c8ec72'
Client: READ b'55904b6ac4400c44f73e451da0f129b20c2110c85e696b8c4
0ddedd167ce1f7915025a087daa9ef4be8c07e4f21c38962e834b800647435fd
3b90747b2810eb9'
Server: FLUSHING b'e40c4c714116e60cdef171bb6c0feaa255dff1c507c2c
4439ec9605b7e0ba9fc54bae39355cb89fd6ebe5841d673c7b7bc68a46f575a3
12eebd220d4b32441bd'
Client: DECOMPRESSED b'Lorem ipsum dolor sit amet, consectetuer
adi'
Client: READ b'c4bbcc13ac123bded6e4bef1c91ee40d3c6580e3ff52aad2e
8cb2eb6062dad74a89ca904cbb0f2545e0db4b1f2e01955b8c511cb2ac08967d
228af1447c8ec72'
Client: DECOMPRESSED b'piscing elit. Donec\negestas, enim et con
sectetuer ullamcorper, lectus ligula rutrum leo, a\nelementum el
it tortor eu quam. Duis tinci'
Client: READ b'e40c4c714116e60cdef171bb6c0feaa255dff1c507c2c4439
ec9605b7e0ba9fc54bae39355cb89fd6ebe5841d673c7b7bc68a46f575a312ee
bd220d4b32441bd'
Client: DECOMPRESSED b'dunt nisi ut ante. Nulla\nfacilisi. Sed t
ristique eros eu libero. Pellentesque vel arcu. Vivamus\npurus o
rci, iaculis ac'
Server: FLUSHING b'c1b36ebf0aedef3d57ea4b26dd986dd39af57dfb05d32
279de'
Client: READ b'c1b36ebf0aedef3d57ea4b26dd986dd39af57dfb05d32279d
e'
Client: DECOMPRESSED b', suscipit sit amet, pulvinar eu,\nlacus.
\n'
Client: response matches file contents: True

Ver también