bz2 — Compresión bzip2

Propósito:Compresión bzip2

El módulo bz2 es una interfaz para la biblioteca bzip2, utilizada para comprimir datos para su almacenamiento o transmisión. Están previstas tres interfaces de programación:

  • Funciones de compresión/descompresión «de una acción» para operar en un blob de datos
  • Objetos iterativos de compresión/descompresión para trabajar con flujos de datos
  • una clase similar a un archivo que admite la lectura y la escritura como con un archivo sin comprimir

Operaciones de una acción en la memoria

La forma más sencilla de trabajar con bz2 es cargar todos los datos a comprimirse o descomprimirse en la memoria, y luego usar compress() y decompress() para transformarlos.

bz2_memory.py
import bz2
import binascii

original_data = b'This is the original text.'
print('Original     : {} bytes'.format(len(original_data)))
print(original_data)

print()
compressed = bz2.compress(original_data)
print('Compressed   : {} bytes'.format(len(compressed)))
hex_version = binascii.hexlify(compressed)
for i in range(len(hex_version) // 40 + 1):
    print(hex_version[i * 40:(i + 1) * 40])

print()
decompressed = bz2.decompress(compressed)
print('Decompressed : {} bytes'.format(len(decompressed)))
print(decompressed)

Los datos comprimidos contienen caracteres no ASCII, por lo que deben ser convertidos a su representación hexadecimal antes de que se puedan imprimir. En la salida de estos ejemplos, la versión hexadecimal es reformateada para tener como máximo 40 caracteres en cada línea.

$ python3 bz2_memory.py

Original     : 26 bytes
b'This is the original text.'

Compressed   : 62 bytes
b'425a683931415926535916be35a6000002938040'
b'01040022e59c402000314c000111e93d434da223'
b'028cf9e73148cae0a0d6ed7f17724538509016be'
b'35a6'

Decompressed : 26 bytes
b'This is the original text.'

Para texto corto, la versión comprimida puede ser significativamente más larga que el original. Mientras que los resultados reales dependen de los datos de entrada, Es interesante observar la sobrecarga de compresión.

bz2_lengths.py
import bz2

original_data = b'This is the original text.'

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

for i in range(5):
    data = original_data * i
    compressed = bz2.compress(data)
    print(fmt.format(len(data), len(compressed)), end='')
    print('*' if len(data) < len(compressed) else '')

Las líneas de salida que terminan con * muestran los puntos donde los datos comprimidos son más grandes que la entrada en bruto.

$ python3 bz2_lengths.py

      len(data)  len(compressed)
---------------  ---------------
              0               14*
             26               62*
             52               68*
             78               70
            104               72

Compresión y descompresión incremental

El enfoque en memoria tiene inconvenientes obvios que lo hacen poco práctico para casos de uso en el mundo real. La alternativa es usar los objetos BZ2Compressor y BZ2Decompressor para manipular los datos de manera incremental donde no es necesario que todo el conjunto de datos encaje en la memoria.

bz2_incremental.py
import bz2
import binascii
import io

compressor = bz2.BZ2Compressor()

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 vacía. Cuando todos los datos están presentes, el método flush() fuerza al compresor a cerrar el bloque final y devolver el resto de los datos comprimidos.

$ python3 bz2_incremental.py

buffering...
buffering...
buffering...
buffering...
Flushed: b'425a6839314159265359ba83a48c000014d5800010400504052fa
7fe003000ba9112793d4ca789068698a0d1a341901a0d53f4d1119a8d4c9e812
d755a67c10798387682c7ca7b5a3bb75da77755eb81c1cb1ca94c4b6faf209c5
2a90aaa4d16a4a1b9c167a01c8d9ef32589d831e77df7a5753a398b11660e392
126fc18a72a1088716cc8dedda5d489da410748531278043d70a8a131c2b8adc
d6a221bdb8c7ff76b88c1d5342ee48a70a12175074918'

Flujos de contenido mixto

BZ2Decompressor también se puede utilizar en situaciones donde los datos comprimidos y sin comprimir están mezclados juntos.

bz2_mixed.py
import bz2

lorem = open('lorem.txt', 'rt').read().encode('utf-8')
compressed = bz2.compress(lorem)
combined = compressed + lorem

decompressor = bz2.BZ2Decompressor()
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 bz2_mixed.py

Decompressed matches lorem: True
Unused data matches lorem : True

Escribir archivos comprimidos

BZ2File puede usarse para escribir y leer desde archivos comprimidos con bzip2 utilizando los métodos habituales para escribir y leer datos.

bz2_file_write.py
import bz2
import io
import os

data = 'Contents of the example file go here.\n'

with bz2.BZ2File('example.bz2', 'wb') as output:
    with io.TextIOWrapper(output, encoding='utf-8') as enc:
        enc.write(data)

os.system('file example.bz2')

Para escribir datos en un archivo comprimido, abre el archivo con el modo 'wb'. Este ejemplo envuelve el BZ2File con un TextIOWrapper del módulo io para codificar texto Unicode a bytes adecuados para la compresión.

$ python3 bz2_file_write.py

example.bz2: bzip2 compressed data, block size = 900k

Se pueden usar diferentes niveles de compresión al pasar un argumento compresslevel. Los valores válidos van desde 1 hasta 9, inclusive. Los valores mas bajos son más rápidos y dan como resultado una menor compresión. Los valores más altos son más lentos y comprimen más, hasta cierto punto.

bz2_file_compresslevel.py
import bz2
import io
import os

data = open('lorem.txt', 'r', encoding='utf-8').read() * 1024
print('Input contains {} bytes'.format(
    len(data.encode('utf-8'))))

for i in range(1, 10):
    filename = 'compress-level-{}.bz2'.format(i)
    with bz2.BZ2File(filename, 'wb', compresslevel=i) as output:
        with io.TextIOWrapper(output, encoding='utf-8') as enc:
            enc.write(data)
    os.system('cksum {}'.format(filename))

La columna central de números en la salida de la secuencia de comandos es el tamaño en bytes de los archivos producidos. Para estos datos de entrada, los valores de compresión mayores no siempre se pagan en un espacio de almacenamiento reducido. para los mismos datos de entrada. Los resultados variarán para otros entradas.

$ python3 bz2_file_compresslevel.py

3018243926 8771 compress-level-1.bz2
1942389165 4949 compress-level-2.bz2
2596054176 3708 compress-level-3.bz2
1491394456 2705 compress-level-4.bz2
1425874420 2705 compress-level-5.bz2
2232840816 2574 compress-level-6.bz2
447681641 2394 compress-level-7.bz2
3699654768 1137 compress-level-8.bz2
3103658384 1137 compress-level-9.bz2
Input contains 754688 bytes

Una instancia BZ2File también incluye un método writelines() que se puede utilizar para escribir una secuencia de cadenas de texto.

bz2_file_writelines.py
import bz2
import io
import itertools
import os

data = 'The same line, over and over.\n'

with bz2.BZ2File('lines.bz2', 'wb') as output:
    with io.TextIOWrapper(output, encoding='utf-8') as enc:
        enc.writelines(itertools.repeat(data, 10))

os.system('bzcat lines.bz2')

Las líneas deben terminar en un carácter de nueva línea, como cuando se escriben en un archivo regular.

$ python3 bz2_file_writelines.py

The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.
The same line, over and over.

Leer archivos comprimidos

Para volver a leer datos de archivos previamente comprimidos, abre el archivo con modo de lectura ('rb'). El valor devuelto desde read() será una cadena de bytes.

bz2_file_read.py
import bz2
import io

with bz2.BZ2File('example.bz2', 'rb') as input:
    with io.TextIOWrapper(input, encoding='utf-8') as dec:
        print(dec.read())

Este ejemplo lee el archivo escrito por bz2_file_write.py de la sección previa. El BZ2File está envuelto con un TextIOWrapper para decodificar bytes leídos a texto Unicode.

$ python3 bz2_file_read.py

Contents of the example file go here.

Al leer un archivo, también es posible buscar y leer solo una parte de los datos.

bz2_file_seek.py
import bz2
import contextlib

with bz2.BZ2File('example.bz2', 'rb') as input:
    print('Entire file:')
    all_data = input.read()
    print(all_data)

    expected = all_data[5:15]

    # rewind to beginning
    input.seek(0)

    # move ahead 5 bytes
    input.seek(5)
    print('Starting at position 5 for 10 bytes:')
    partial = input.read(10)
    print(partial)

    print()
    print(expected == partial)

La posición seek() es relativa a los datos sin comprimir, por lo que la persona que llama no necesita saber que el archivo de datos está comprimido. Esto permite que una instancia BZ2File se pase a una función que espera un archivo normal sin comprimir.

$ python3 bz2_file_seek.py

Entire file:
b'Contents of the example file go here.\n'
Starting at position 5 for 10 bytes:
b'nts of the'

True

Leer y escribir de datos Unicode

Los ejemplos anteriores usaron BZ2File directamente y manejaron la codificación y decodificación de cadenas de texto Unicode en línea con un io.TextIOWrapper, cuando era necesario. Estos pasos adicionales pueden ser evitados usando bz2.open(), que configura un io.TextIOWrapper para manejar la codificación o decodificación automáticamente.

bz2_unicode.py
import bz2
import os

data = 'Character with an åccent.'

with bz2.open('example.bz2', 'wt', encoding='utf-8') as output:
    output.write(data)

with bz2.open('example.bz2', 'rt', encoding='utf-8') as input:
    print('Full file: {}'.format(input.read()))

# Move to the beginning of the accented character.
with bz2.open('example.bz2', 'rt', encoding='utf-8') as input:
    input.seek(18)
    print('One character: {}'.format(input.read(1)))

# Move to the middle of the accented character.
with bz2.open('example.bz2', 'rt', encoding='utf-8') as input:
    input.seek(19)
    try:
        print(input.read(1))
    except UnicodeDecodeError:
        print('ERROR: failed to decode')

El identificador de archivo devuelto por open() admite seek(), pero ten cuidado porque el puntero del archivo se mueve por bytes no caracteres y puede terminar en medio de un carácter codificado.

$ python3 bz2_unicode.py

Full file: Character with an åccent.
One character: å
ERROR: failed to decode

Comprimir datos de red

El código del siguiente ejemplo responde a solicitudes que consisten en nombres de archivos que escriben una versión comprimida del archivo en el conector que se utiliza para comunicarse con el cliente. Tiene alguna fragmentación artificial para ilustrar el almacenamiento en buffer que se produce cuando los datos pasados a compress() o decompress() no dan como resultado un bloque completo de salida comprimida o sin comprimir.

bz2_server.py
import bz2
import logging
import socketserver
import binascii

BLOCK_SIZE = 32


class Bz2RequestHandler(socketserver.BaseRequestHandler):

    logger = logging.getLogger('Server')

    def handle(self):
        compressor = bz2.BZ2Compressor()

        # Find out what file the client wants
        filename = self.request.recv(1024).decode('utf-8')
        self.logger.debug('client asked for: "%s"', 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

El programa principal inicia un servidor en un hilo, combinando SocketServer y Bz2RequestHandler.

if __name__ == '__main__':
    import socket
    import sys
    from io import StringIO
    import threading

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

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

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

    logger = logging.getLogger('Client')

    # Connect to the server
    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 = (sys.argv[0]
                      if len(sys.argv) > 1
                      else 'lorem.txt')
    logger.debug('sending filename: "%s"', requested_file)
    len_sent = s.send(requested_file.encode('utf-8'))

    # Receive a response
    buffer = StringIO()
    decompressor = bz2.BZ2Decompressor()
    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.
        decompressed = decompressor.decompress(response)
        if decompressed:
            logger.debug('DECOMPRESSED %r', decompressed)
            buffer.write(decompressed.decode('utf-8'))
        else:
            logger.debug('BUFFERING')

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

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

A continuación, abre un conector para comunicarse con el servidor como cliente, y solicita el archivo (por defecto lorem.txt) que contiene:

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo,
a elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi.

Advertencia

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

Ejecutar bz2_server.py produce:

$ python3 bz2_server.py

Client: Contacting server on 127.0.0.1:57364
Client: sending filename: "lorem.txt"
Server: client asked for: "lorem.txt"
Server: RAW b'Lorem ipsum dolor sit amet, cons'
Server: BUFFERING
Server: RAW b'ectetuer adipiscing elit. Donec\n'
Server: BUFFERING
Server: RAW b'egestas, enim et consectetuer ul'
Server: BUFFERING
Server: RAW b'lamcorper, lectus ligula rutrum '
Server: BUFFERING
Server: RAW b'leo,\na elementum elit tortor eu '
Server: BUFFERING
Server: RAW b'quam. Duis tincidunt nisi ut ant'
Server: BUFFERING
Server: RAW b'e. Nulla\nfacilisi.\n'
Server: BUFFERING
Server: FLUSHING b'425a6839314159265359ba83a48c000014d5800010400
504052fa7fe003000ba'
Server: FLUSHING b'9112793d4ca789068698a0d1a341901a0d53f4d1119a8
d4c9e812d755a67c107'
Client: READ b'425a6839314159265359ba83a48c000014d58000104005040
52fa7fe003000ba'
Server: FLUSHING b'98387682c7ca7b5a3bb75da77755eb81c1cb1ca94c4b6
faf209c52a90aaa4d16'
Client: BUFFERING
Server: FLUSHING b'a4a1b9c167a01c8d9ef32589d831e77df7a5753a398b1
1660e392126fc18a72a'
Client: READ b'9112793d4ca789068698a0d1a341901a0d53f4d1119a8d4c9
e812d755a67c107'
Server: FLUSHING b'1088716cc8dedda5d489da410748531278043d70a8a13
1c2b8adcd6a221bdb8c'
Client: BUFFERING
Server: FLUSHING b'7ff76b88c1d5342ee48a70a12175074918'
Client: READ b'98387682c7ca7b5a3bb75da77755eb81c1cb1ca94c4b6faf2
09c52a90aaa4d16'
Client: BUFFERING
Client: READ b'a4a1b9c167a01c8d9ef32589d831e77df7a5753a398b11660
e392126fc18a72a'
Client: BUFFERING
Client: READ b'1088716cc8dedda5d489da410748531278043d70a8a131c2b
8adcd6a221bdb8c'
Client: BUFFERING
Client: READ b'7ff76b88c1d5342ee48a70a12175074918'
Client: DECOMPRESSED b'Lorem ipsum dolor sit amet, consectetuer
adipiscing elit. Donec\negestas, enim et consectetuer ullamcorpe
r, lectus ligula rutrum leo,\na elementum elit tortor eu quam. D
uis tincidunt nisi ut ante. Nulla\nfacilisi.\n'
Client: response matches file contents: True

Ver también