9.4. bz2 — bzip2 压缩

未匹配的标注

目的:bzip2压缩

bz2 模块是 bzip2 库的接口,用于压缩数据以进行存储或传输。提供了三种API:

  • “一次性” 压缩/解压缩功能,用于操作大堆数据
  • 用于处理数据流的迭代压缩/解压缩对象
  • 类似文件的类,支持与未压缩文件一样的读写

内存中的一次性操作

使用 bz2 的最简单方法是在内存中加载要压缩或解压缩的所有数据,然后使用 compress()decompress() 对其进行转换。

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)

压缩数据包含非 ASCII 字符,因此需要先将其转换为十六进制表示,然后才能打印。在这些示例的输出中,十六进制版本被重新格式化为最多 40 个字符的行。

$ 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.'

对于简短文本,压缩版本可能比原始版本长得多。虽然实际结果取决于输入数据,但观察压缩开销很有意思。

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

*结尾的输出行显示压缩数据比原始输入长的点。

$ python3 bz2_lengths.py

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

增量压缩和解压缩

在内存中进行一次性压缩 / 解压缩的操作有明显缺陷,所以不能切实应用于实际中。考虑使用 BZ2Compressor 以及 BZ2Decompressor 对象来逐步操作数据,可避免将所有数据一次性放入内存。

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

该示例演示从纯文本文件中读取小块数据并将其传入 compress()。压缩器维护压缩数据的内部缓冲区。由于压缩算法取决于校验码以及最小块大小,因此压缩器每次接收到更多输入时可能无法返回数据。如果没有准备好整个压缩块,压缩器会返回空字符串。当所有数据被输入时, flush() 方法令压缩器强制关闭最后一个数据块,并返回剩余的压缩数据。

$ python3 bz2_incremental.py

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

混合内容流

BZ2Decompressor 也可以用于压缩数据与未压缩数据混合在一起的情况。

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)

在解压缩全部数据后, unused_data  属性将保存所有未使用的数据内容。

$ python3 bz2_mixed.py

Decompressed matches lorem: True
Unused data matches lorem : True

写入压缩文件

BZ2File 可用于写入或读取以 bzip2 格式压缩的文件,并用通常的方法读写数据。

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

当需要往压缩文件内写入数据时,先将其以 'wb' 模式打开。本例中用来自 io 模块的  TextIOWrapper 包装了 BZ2File 使其能够把 Unicode 字符编码为字节码,以适应后续的压缩过程。

$ python3 bz2_file_write.py

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

通过传入一个 compresslevel 参数可以选择调用不同的压缩级别。该参数可取值范围为 1 到 9 的闭区间。取值越小,则压缩速度越快,压缩幅度越低。取值越大则速度越慢、幅度越大,最大值可以将文件压缩至一个点。

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

本段代码运行结果的中间一列数字是产生的文件的大小,单位为字节。对于此类输入数据而言,越高的压缩率并不意味着可以抵消越少的存储空间带来的限制。结果会因输入数据的不同而变化。

$ 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

BZ2File 实例中也包含了 writelines() 方法,可以用来写入字符串序列。

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

字符串当以换行符结尾,同写入常规文本文件一样。

$ 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.

读取压缩的文件

从先前已经压缩的文件中读取数据,需要使用 'rb' 模式进行打开。它会以字符串形式从 read() 方法中返回。

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

这个示例,是读取上一节 bz2_file_write.py 中被压缩的文件。 BZ2File 中的 TextIOWrapper 是用来解析读取到 Unicode 文本的字节。

$ python3 bz2_file_read.py

Contents of the example file go here.

在读取文件的时候,也可以进行查找和读取部分数据。

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]

    # 回到起始位置
    input.seek(0)

    # 往前移动 5 字节
    input.seek(5)
    print('Starting at position 5 for 10 bytes:')
    partial = input.read(10)
    print(partial)

    print()
    print(expected == partial)

seek() 定位是作用于 未解压  的数据,所以调用者不需要知道文件是否压缩。这就允许 BZ2File 的实例传递给一些等待处理未压缩文件的函数。

$ 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

读写 Unicode 数据

前面的例子直接使用 BZ2File 来读写压缩文件,并使用 io.TextIOWrapper 来管理 Unicode 文本的编解码,必要时使用 bz2.open() 来避免这些额外的步骤,可以通过设置 io.TextIOWrapper 来自动处理编码解码。

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

# 移动到重音字符前
with bz2.open('example.bz2', 'rt', encoding='utf-8') as input:
    input.seek(18)
    print('One character: {}'.format(input.read(1)))

# 移动到重音字符中间
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')

open() 方法返回的文件句柄可以用 seek() 方法来移动指针,但务必小心,因为这移动的是 字节 而不是 字符,而且有可能会被移动到一个字符的中间。

$ python3 bz2_unicode.py

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

压缩网络上的数据

下面的示例代码能响应由一系列文件名组成的请求,并将压缩后的对应文件写入套接字来与客户端进行交流。程序中通过人工分块来说明缓冲的原理:当你把数据传给 compress() 或 decompress() 时,压缩完或解压完的数据输出不能立刻输出,此时就需要缓冲。

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

        # 弄清楚客户端想要什么文件
        filename = self.request.recv(1024).decode('utf-8')
        self.logger.debug('client asked for: "%s"', filename)

        # 将文件分块压缩后发送
        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')

        # 发送由压缩程序缓冲的数据
        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

主程序结合了 SocketServer 与 Bz2RequestHandler,它会在线程中启动一个服务端。

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

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

    # 启动服务端,并在独立的线程中运行
    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')

    # 连接到服务端
    logger.info('Contacting server on %s:%s', ip, port)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # 请求一个文件
    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'))

    # 接收响应
    buffer = StringIO()
    decompressor = bz2.BZ2Decompressor()
    while True:
        response = s.recv(BLOCK_SIZE)
        if not response:
            break
        logger.debug('READ %r', binascii.hexlify(response))

        # 在给解压程序传递数据时,会包含任何未耗尽的数据
        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)

    # 清理工作
    server.shutdown()
    server.socket.close()
    s.close()

之后它会打开套接字作为客户端与服务端通信。运行程序时需要包含了以下内容的文件,默认文件名为 lorem.txt

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.

警告

这种实现有明显的安全隐患。不要在公网的服务器或者其他可能有安全风险的环境中运行示例代码。

运行 bz2_server.py 文件会输出:

$ 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

参见

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/pymotw/bz2-bzip...

译文地址:https://learnku.com/docs/pymotw/bz2-bzip...

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~