bz2 - Compressione bzip2

Scopo: Il modulo bz2 è una interfaccia per la libreria bzip2, usata per comprimere dati per conservazione o trasmissione.

Sono fornite tre API:

  • - funzioni di compressione/decompressione "una tantum" per operare su di una massa di dati
  • - oggetti di compressione/decompressione interattivi per lavorare con flussi di dati
  • - una classe tipo file che supporta la lettura e la scrittura come un file non compresso

Operazioni in Memoria Una Tantum

Il modo più semplice per lavorare con bz2 è di caricare tutti i dati da comprimere o decomprimere in memoria, quindi usare compress() e decompress() per trasformarli.

# bz2_memory.py

import bz2
import binascii

original_data = b"Questo e' il testo originale."
print('Originale    : {} byte'.format(len(original_data)))
print(original_data)

print()
compressed = bz2.compress(original_data)
print('Compresso   : {} byte'.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('Decompresso : {} byte'.format(len(decompressed)))
print(decompressed)

I dati compressi contengono caratteri non ASCII, quindi devono essere convertiti nella rappresentazione esadecimale prima di essere stampati.

$ python3 bz2_memory.py

Originale    : 29 byte
b"Questo e' il testo originale."

Compresso   : 63 byte
b'425a6839314159265359b99a9fef000003138040'
b'81200022a59e00200021a99320c9e50a600029db'
b'bb135a84bc64e4b488bb0e82f8bb9229c28485cc'
b'd4ff78'

Decompresso : 29 byte
b"Questo e' il testo originale."

Per testi brevi, la dimensione della versione compressa può essere significativamente più grande di quella originale. Mentre il risultato reale dipende dai dati in input, è interessante osservare il ricarico di compressione.

# bz2_lengths.py

import bz2

original_data = b"Questo e' il testo originale."

fmt = '{:>15}  {:>15}'
print(fmt.format('len(dati)', '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 '')

Le righe che nel risultato finiscono con * mostrano i punti dove la dimensione dei dati compressi è più grande degli originali.

$ python3 bz2_lengths.py

      len(dati)  len(compressed)
---------------  ---------------
              0               14*
             29               63*
             58               70*
             87               71
            116               73

Compressione e Decompressione Incrementale

L'approccio "in memoria" ha ovvi inconvenienti che lo rendono improponibile per usi pratici. L'alternativa è di usare gli oggetti BZ2Compressor e BZ2Decompressor per manipolare i dati in modo incrementale così che l'intero insieme di dati non debba essere accomodato in 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('Compressi: {}'.format(
                binascii.hexlify(compressed)))
        else:
            print('accumulo...')
    remaining = compressor.flush()
    print('Svuotamento: {}'.format(binascii.hexlify(remaining)))

Questo esempio legge piccoli blocchi di dati da un file a testo semplice e li passa a compress(). Il compressore mantiene un buffer interno di dati compressi. Visto che l'algoritmo di compressione dipende da cifre di controllo e dimensioni minime di blocco, il compressore potrebbe non essere pronto per ritornare dati ogni volta che ne riceve ulteriori in input. Se non ha un intero blocco compresso pronto, ritorna una stringa vuota. Quando tutti i dati sono stati passati, il metodo flush() forza il compressore a chiudere il blocco finale e ritornare i dati compressi rimasti.

$ python3 bz2_incremental.py

accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
accumulo...
Svuotamento: b'425a6839314159265359ef75176500004357800010400524074b003ff7ff004001d1c6820d0988a7e5349e88d000d06a6342298d4d3401a640634c4611a6000004a6441354f4c9a483d47a87a8306efbf1e657e1e92000a3e1a0eab2c3505d85acfe24c14151142e5e597e9046c9886d5ab53365e0e9020b30afbc8acbe3be0d6896aa43cc54e6caeb75c860ba68888294a85badd1d3380a36c16c66ee43dee7fa24ef3c7690db6d173cd14573633d4854be4bdcc60e119c0cca77ab4d82be33b5e123e2f22dd9eb304f0c48f46f5dffa66ea8d7766e5fea2f79df1d6c790d5ce94dcb1d5471e1e115f3072d55d8f1887a374be707659a519b2ad7135d551b83064d52e690a760a2e2a6c99c6bdc6fbf3b62375a387a3182475f2e57acf8c436a19f8198554e4cc61d287670a2caa24bbb509a24ab53414bc2ebb75faf05e385f6dbf76a8dbc68785c9984849525bf270a197f2c929bf3d46e5152dc4ceafaba28bb702a1a7a52cfb988c58625624bd0c112fb59d14ad2dfb9c245454c33215b23916d45992d08456c2aa8c0c8ebb88bf8a0081d76d9edf84ce994f178925732e6e622de86f2fa1203ec9fc5dc914e14243bdd45d940'

Flussi di Contenuto Misto

BZ2Decompressor può anche essere usato in situazioni dove dati compressi e non compressi sono mescolati.

# 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('Decompressi che corrispondono a lorem       :', decompressed_matches)

unused_matches = decompressor.unused_data == lorem
print('Dati inutilizzati che corrispondono a lorem :', unused_matches)

Dopo la decompressione di tutti i dati, l'attributo unused_data contiene qualunque dato che non sia stato usato.

$ python3 bz2_mixed.py

Decompressi che corrispondono a lorem       : True
Dati inutilizzati che corrispondono a lorem : True

Scrivere Dati Compressi

BZ2File può anche essere usato per scrivere verso e leggere da file compressi con bzip2 usando gli abituali metodi per leggere e scrivere dati..

# bz2_file_write.py

import bz2
import io
import os

data = 'Il contenuto del file esempio va qui.\n'

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

os.system('file esempio.bz2')

Per scrivere dati in un file compresso, lo si apra con modalità 'wb'. Questo esempio impacchetta il file aperto con BZ2File con un TextIOWrapper dal modulo io per codificare testo Unicode in byte adatti alla compressione.

$ python3 bz2_file_write.py

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

E' possibile usare diversi livelli di compressione tramite l'argomento compresslevel. L'intervallo di valori valido è da 1 a 9 compreso. Valori bassi generano una compressione più veloce ma di maggiori dimensioni. Valori alti producono una compressione più lenta e più compatta.

# bz2_file_compresslevel.py

import bz2
import io
import os

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

for i in range(1, 10):
    filename = 'livello-di-compressione-{}.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 colonna centrale di cifre nel risultato dello script rappresenta la dimensione in byte dei file prodotti. Per questi dati in input, valori di compression più alti non sempre si traducono in uno spazio di conservazione minore per gli stessi dati in input. I risultati possono variare per altri input.

$ python3 bz2_file_compresslevel.py

Input coniene  754688 byte
4105199836 8722 livello-di-compressione-1.bz2
2875524866 4918 livello-di-compressione-2.bz2
2466635111 3681 livello-di-compressione-3.bz2
2441059017 2697 livello-di-compressione-4.bz2
348190677 2687 livello-di-compressione-5.bz2
1474295117 2556 livello-di-compressione-6.bz2
4077188232 2382 livello-di-compressione-7.bz2
3725855634 1129 livello-di-compressione-8.bz2
3022461369 1129 livello-di-compressione-9.bz2

Una istanza di BZ2File include anche un metodo writelines() che può essere usato per scrivere una sequenza di stringhe.

# bz2_file_writelines.py

import bz2
import io
import itertools
import os

data = 'La stessa riga, ripetutamente.\n'

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

os.system('bzcat righe.bz2')

Le righe dovrebbero finire con un carattere di ritorno a capo, come per la scrittura di un file normale.

$ python3 bz2_file_writelines.py

La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.
La stessa riga, ripetutamente.

Leggere File Compressi

Per leggere dati da file precedentemente compressi, si apra il file in modalità lettura ('rb'). Il valore ritornato da read() sarà una stringa di byte.

# bz2_file_read.py

import bz2
import io

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

Questo esempio legge il file scritto da bz2_file_write.py dalla sezione precedente. Il file aperto con BZ2File viene impacchettato con un TextIOWrapper per decodificare i byte letti in testo Unicode.

$ python3 bz2_file_read.py

Il contenuto del file esempio va qui.

Quando si sta leggendo un file, è possibile anche spostarsi al suo interno, e leggere solo parte dei dati.

# bz2_file_seek.py

import bz2
import contextlib

with bz2.BZ2File('esempio.bz2', 'rb') as input:
    print('Tutto il file:')
    all_data = input.read()
    print(all_data)

    expected = all_data[5:15]

    # mi porto all'inizio
    input.seek(0)

    # mi sposto in avanti di 5 byte
    input.seek(5)
    print('A partire dalla posizione 5 per 10 byte:')
    partial = input.read(10)
    print(partial)

    print()
    print(expected == partial)

La posizione di seek() è relativa ai dati non compressi, quindi il chiamante non deve preoccuparsi di sapere che il file è compresso. Questo consente di passare l'istanza di BZ2File ad una funzione che si attende un normale file non compresso.

$ python3 bz2_file_seek.py

Tutto il file:
b'Il contenuto del file esempio va qui.\n'
A partire dalla posizione 5 per 10 byte:
b'ntenuto de'

True

Leggere e Scrivere Dati Unicode

Gli esempi precedenti usano BZ2File direttamente e gestiscono la codifica e decodifica delle stringhe di testo Unicode con un TextIOWrapper in linea, dove necessario. Questi passi supplementari possono essere evitati usando bz2.open(), che imposta un io.TextIOWrapper per gestire la codifica e decodifica automaticamente.

# bz2_unicode.py

import bz2
import os

data = 'Carattere accentato å'

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

with bz2.open('esempio.bz2', 'rt', encoding='utf-8') as input:
    print('File completo: {}'.format(input.read()))

# Spostamento all'inizio del carattere accentato.
with bz2.open('esempio.bz2', 'rt', encoding='utf-8') as input:
    input.seek(20)
    print('Un carattere: {}'.format(input.read(1)))

# Spostamento in mezzo al carattere accentato
with bz2.open('esempio.bz2', 'rt', encoding='utf-8') as input:
    input.seek(21)
    try:
        print(input.read(1))
    except UnicodeDecodeError:
        print('ERRORE: decodifica fallita')

L'handle del file ritornato da opern() supporta seek(), ma si raccomanda prudenza in quanto il puntatore del file si sposta per byte e non per caratteri, e potrebbe non trovarsi nel mezzo di un carattere codificato.

$ python3 bz2_unicode.py

File completo: Carattere accentato å
Un carattere: å
ERRORE: decodifica fallita

Comprimere Dati in Rete

L'esempio seguente risponde a richieste che contengono nomi di file scrivendo una versione compressa del file verso il socket usato per comunicare con il client. Ha impostata una spezzettatura artificiale sul posto per illustrare l'accumulo quando i dati passati da compress() a decompress() non costituiscono un blocco completo di output compresso o decompresso.

# bz2_server.py
# -*- coding: utf-8 -*-
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()

        # Cosa vuole il client?
        filename = self.request.recv(1024).decode('utf-8')
        self.logger.debug('il client richiede: "%s"', filename)

        # Invio di pezzi del file mentre si stanno comprimento
        with open(filename, 'rb') as input:
            while True:
                block = input.read(BLOCK_SIZE)
                if not block:
                    break
                self.logger.debug('GREZZI %r', block)
                compressed = compressor.compress(block)
                if compressed:
                    self.logger.debug(
                        'IN INVIO %r',
                        binascii.hexlify(compressed))
                    self.request.send(compressed)
                else:
                    self.logger.debug('ACCUMULO')

        # Invia tutti i dati accumulati al compressore
        remaining = compressor.flush()
        while remaining:
            to_send = remaining[:BLOCK_SIZE]
            remaining = remaining[BLOCK_SIZE:]
            self.logger.debug('SCARICAMENTO %r',
                              binascii.hexlify(to_send))
            self.request.send(to_send)
        return

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

    # Connessione al server
    logger.info('Contatto il server us %s:%s', ip, port)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # Richiesta di un  file
    requested_file = (sys.argv[0]
                      if len(sys.argv) > 1
                      else 'lorem.txt')
    logger.debug('invio del nome del file: "%s"', requested_file)
    len_sent = s.send(requested_file.encode('utf-8'))

    # Riceve una risposta
    buffer = StringIO()
    decompressor = bz2.BZ2Decompressor()
    while True:
        response = s.recv(BLOCK_SIZE)
        if not response:
            break
        logger.debug('LETTURA %r', binascii.hexlify(response))

        # Include tutti i dati non consumati quando si
        #  alimenta il decompressore.
        decompressed = decompressor.decompress(response)
        if decompressed:
            logger.debug('DECOMPRESSI %r', decompressed)
            buffer.write(decompressed.decode('utf-8'))
        else:
            logger.debug('ACCUMULO')

    full_response = buffer.getvalue()
    lorem = open(requested_file, 'rt').read()
    logger.debug('la risposta corrisponde al contenuto del file: %s',
                 full_response == lorem)

    # Pulizia
    server.shutdown()
    server.socket.close()
    s.close()

Il programma principale fa partire un server in un thread, combinando SocketServer e Bz2RequestHandler quindi apre un socket per comunicare con il server come un client, e richiede il file (predefinito lorem.txt).

Questa implementazione ha ovvi problemi di sicurezza. Non eseguirlo in un server aperto su Internet od in un qualsiasi ambiente nel quale la sicurezza potrebbe essere un problema.
$ python3 bz2_server.py

Client: Contatto il server us 127.0.0.1:57563
Client: invio del nome del file: "lorem.txt"
Server: il client richiede: "lorem.txt"
Server: GREZZI b'Lorem ipsum dolor sit amet, cons'
Server: ACCUMULO
Server: GREZZI b'ectetuer adipiscing elit.\nDonec '
Server: ACCUMULO
Server: GREZZI b'egestas, enim et consectetuer ul'
Server: ACCUMULO
Server: GREZZI b'lamcorper, lectus ligula rutrum '
Server: ACCUMULO
Server: GREZZI b'leo,\na elementum elit tortor eu '
Server: ACCUMULO
Server: GREZZI b'quam. Duis tincidunt nisi ut ant'
Server: ACCUMULO
Server: GREZZI b'e. Nulla\nfacilisi. Sed tristique'
Server: ACCUMULO
Server: GREZZI b' eros eu libero. Pellentesque ve'
Server: ACCUMULO
Server: GREZZI b'l\narcu. Vivamus purus orci, iacu'
Server: ACCUMULO
Server: GREZZI b'lis ac, suscipit sit amet, pulvi'
Server: ACCUMULO
Server: GREZZI b'nar eu,\nlacus. Praesent placerat'
Server: ACCUMULO
Server: GREZZI b' tortor sed nisl. Nunc blandit d'
Server: ACCUMULO
Server: GREZZI b'iam egestas\ndui. Pellentesque ha'
Server: ACCUMULO
Server: GREZZI b'bitant morbi tristique senectus '
Server: ACCUMULO
Server: GREZZI b'et netus et\nmalesuada fames ac t'
Server: ACCUMULO
Server: GREZZI b'urpis egestas. Aliquam viverra f'
Server: ACCUMULO
Server: GREZZI b'ringilla\nleo. Nulla feugiat augu'
Server: ACCUMULO
Server: GREZZI b'e eleifend nulla. Vivamus mauris'
Server: ACCUMULO
Server: GREZZI b'. Vivamus sed\nmauris in nibh pla'
Server: ACCUMULO
Server: GREZZI b'cerat egestas. Suspendisse poten'
Server: ACCUMULO
Server: GREZZI b'ti. Mauris\nmassa. Ut eget velit '
Server: ACCUMULO
Server: GREZZI b'auctor tortor blandit sollicitud'
Server: ACCUMULO
Server: GREZZI b'in. Suspendisse\nimperdiet justo.'
Server: ACCUMULO
Server: GREZZI b'\n'
Server: ACCUMULO
Server: SCARICAMENTO b'425a6839314159265359ef75176500004357800010400524074b003ff7ff0040'
Server: SCARICAMENTO b'01d1c6820d0988a7e5349e88d000d06a6342298d4d3401a640634c4611a60000'
Client: LETTURA b'425a6839314159265359ef75176500004357800010400524074b003ff7ff0040'
Server: SCARICAMENTO b'04a6441354f4c9a483d47a87a8306efbf1e657e1e92000a3e1a0eab2c3505d85'
Client: ACCUMULO
Server: SCARICAMENTO b'acfe24c14151142e5e597e9046c9886d5ab53365e0e9020b30afbc8acbe3be0d'
Client: LETTURA b'01d1c6820d0988a7e5349e88d000d06a6342298d4d3401a640634c4611a60000'
Server: SCARICAMENTO b'6896aa43cc54e6caeb75c860ba68888294a85badd1d3380a36c16c66ee43dee7'
Client: ACCUMULO
Server: SCARICAMENTO b'fa24ef3c7690db6d173cd14573633d4854be4bdcc60e119c0cca77ab4d82be33'
Client: LETTURA b'04a6441354f4c9a483d47a87a8306efbf1e657e1e92000a3e1a0eab2c3505d85'
Server: SCARICAMENTO b'b5e123e2f22dd9eb304f0c48f46f5dffa66ea8d7766e5fea2f79df1d6c790d5c'
Client: ACCUMULO
Server: SCARICAMENTO b'e94dcb1d5471e1e115f3072d55d8f1887a374be707659a519b2ad7135d551b83'
Client: LETTURA b'acfe24c14151142e5e597e9046c9886d5ab53365e0e9020b30afbc8acbe3be0d'
Server: SCARICAMENTO b'064d52e690a760a2e2a6c99c6bdc6fbf3b62375a387a3182475f2e57acf8c436'
Client: ACCUMULO
Server: SCARICAMENTO b'a19f8198554e4cc61d287670a2caa24bbb509a24ab53414bc2ebb75faf05e385'
Client: LETTURA b'6896aa43cc54e6caeb75c860ba68888294a85badd1d3380a36c16c66ee43dee7'
Server: SCARICAMENTO b'f6dbf76a8dbc68785c9984849525bf270a197f2c929bf3d46e5152dc4ceafaba'
Client: ACCUMULO
Server: SCARICAMENTO b'28bb702a1a7a52cfb988c58625624bd0c112fb59d14ad2dfb9c245454c33215b'
Client: LETTURA b'fa24ef3c7690db6d173cd14573633d4854be4bdcc60e119c0cca77ab4d82be33'
Server: SCARICAMENTO b'23916d45992d08456c2aa8c0c8ebb88bf8a0081d76d9edf84ce994f178925732'
Client: ACCUMULO
Server: SCARICAMENTO b'e6e622de86f2fa1203ec9fc5dc914e14243bdd45d940'
Client: LETTURA b'b5e123e2f22dd9eb304f0c48f46f5dffa66ea8d7766e5fea2f79df1d6c790d5c'
Client: ACCUMULO
Client: LETTURA b'e94dcb1d5471e1e115f3072d55d8f1887a374be707659a519b2ad7135d551b83'
Client: ACCUMULO
Client: LETTURA b'064d52e690a760a2e2a6c99c6bdc6fbf3b62375a387a3182475f2e57acf8c436'
Client: ACCUMULO
Client: LETTURA b'a19f8198554e4cc61d287670a2caa24bbb509a24ab53414bc2ebb75faf05e385'
Client: ACCUMULO
Client: LETTURA b'f6dbf76a8dbc68785c9984849525bf270a197f2c929bf3d46e5152dc4ceafaba'
Client: ACCUMULO
Client: LETTURA b'28bb702a1a7a52cfb988c58625624bd0c112fb59d14ad2dfb9c245454c33215b'
Client: ACCUMULO
Client: LETTURA b'23916d45992d08456c2aa8c0c8ebb88bf8a0081d76d9edf84ce994f178925732'
Client: ACCUMULO
Client: LETTURA b'e6e622de86f2fa1203ec9fc5dc914e14243bdd45d940'
Client: DECOMPRESSI b'Lorem ipsum dolor sit amet, consectetuer adipiscing elit.\nDonec egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo,\na elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla\nfacilisi. Sed tristique eros eu libero. Pellentesque vel\narcu. Vivamus purus orci, iaculis ac, suscipit sit amet, pulvinar eu,\nlacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas\ndui. Pellentesque habitant morbi tristique senectus et netus et\nmalesuada fames ac turpis egestas. Aliquam viverra fringilla\nleo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed\nmauris in nibh placerat egestas. Suspendisse potenti. Mauris\nmassa. Ut eget velit auctor tortor blandit sollicitudin. Suspendisse\nimperdiet justo.\n'
Client: la risposta corrisponde al contenuto del file: True

Vedere anche:

bz2
La documentazione della libreria standard per questo modulo.
bzip2.org
La pagina home per bzip2.
zlib
Il modulo zlib è una interfaccia a più basso livello alla compressione gzip.
bzip
Una intefaccia tipo file per file compressi GNU zip
io
Costruzioni per creare condutture in input ed output
Note di portabilità da 2 a 3 per bzip