CPython bz2 模块完全指南:bzip2 文件、增量与一次性(解)压缩实战
bz2 是 CPython 标准库中基于 libbzip2 提供 bzip2 算法(解)压缩能力的官方模块,源码位于 Lib/bz2.py。本文从该模块的官方参考文档 Doc/library/bz2.rst 出发,系统讲解它的三类接口——面向文件对象的 open/BZ2File、面向增量处理流的 BZ2Compressor/BZ2Decompressor、以及面向内存块的一次性 compress/decompress,并结合 CPython 仓库内的实现源码与单元测试,帮你掌握选择合适接口、正确设置压缩级别、处理多段压缩流与文本/二进制文件读写的完整方法。
模块概览:三种使用形态
bz2 为 bzip2 压缩算法提供了一套"全栈式"接口,模块内共封装五个公开符号(见 Lib/bz2.py 的 __all__):
| 形态 | 接口 | 适用场景 |
|---|---|---|
| 文件(解)压缩 | bz2.open()、bz2.BZ2File |
直接读写磁盘上的 .bz2 文件,支持二进制与文本两种模式 |
| 增量(解)压缩 | bz2.BZ2Compressor、bz2.BZ2Decompressor |
数据以流式/分块方式到达,无法一次性装入内存(如网络传输、管道) |
| 一次性(解)压缩 | bz2.compress()、bz2.decompress() |
数据块完整在内存中,需要快速整体压缩或还原 |
值得注意的是,BZ2Compressor 与 BZ2Decompressor 的真正算法实现并不在纯 Python 层,而是由 CPython 的 C 扩展 _bz2(Modules/_bz2module.c,其文件头注释明确写着 "Low-level Python interface to libbzip2")直接封装 libbzip2 提供。Lib/bz2.py 顶部即通过 from _bz2 import BZ2Compressor, BZ2Decompressor(Lib/bz2.py)把 C 层的加速实现引入 Python 命名空间。
同时请留意:bz2 属于可选模块。若你的 Python 发行版缺失该模块,请向发行商索取文档;如果你是发行商,则需满足 optional-module-requirements 对应的构建要求(详见 Doc/includes/optional-module.rst 中的统一说明)。
文件的(解)压缩
bz2.open():兼顾二进制与文本的便捷入口
函数签名为:
bz2.open(filename, mode='rb', compresslevel=9, encoding=None, errors=None, newline=None)
filename 既可以是真正的文件名(str、bytes 或 path-like object),也可以是一个已经打开的文件对象,此时模块直接在该对象上读写压缩数据,例如配合 io.BytesIO 使用。
mode 支持下列取值:
- 二进制模式:
'r'、'rb'、'w'、'wb'、'x'、'xb'、'a'、'ab'; - 文本模式:
'rt'、'wt'、'xt'、'at'; - 默认模式为
'rb'。
在二进制模式下,bz2.open() 等价于直接构造 BZ2File(filename, mode, compresslevel=compresslevel),此时不得提供 encoding、errors、newline 参数,否则会抛出 ValueError(校验逻辑见 Lib/bz2.py)。在文本模式下,函数内部先创建 BZ2File 对象,再将其包装进一个 io.TextIOWrapper,用指定的编码、错误处理策略与换行符规则对外提供文本流(Lib/bz2.py):
import bz2
with bz2.open("log.txt.bz2", "wt", encoding="utf-8") as f:
f.write("Hello 世界\n" * 1000)
with bz2.open("log.txt.bz2", "rt", encoding="utf-8") as f:
for line in f:
print(line, end="")
BZ2File:面向底层字节的文件接口
class bz2.BZ2File(filename, mode='r', *, compresslevel=9)
BZ2File 提供的是二进制文件接口:读取返回 bytes,写入也须提供 bytes(或任何支持缓冲协议的对象)。构造函数与 open 的关键规则如下:
- 文件名参数:传入
str/bytes/path-like 对象时直接打开对应磁盘文件;传入文件对象时,则使用该对象进行压缩数据的读写(自 Python 3.3 起支持,见 Lib/bz2.py)。 - 模式与语义:
'r'(读,默认)、'w'(覆盖写)、'x'(排他创建,Python 3.4 加入)、'a'(追加,Python 3.3 加入),也可写成带'b'的等价形式'rb'/'wb'/'xb'/'ab'。特例:当filename是文件对象时,'w'不会截断文件,而是等价于'a'(追加)。 - 压缩级别:仅在
'w'/'x'/'a'模式下有意义,compresslevel取 1~9 的整数,1 压缩率最低、9(默认)压缩率最高;该参数自 Python 3.9 起成为仅限关键字参数。若越界,BZ2File.__init__会直接抛出ValueError("compresslevel must be between 1 and 9")(见 Lib/bz2.py)。自 Python 3.9 起,构造时传入的历史遗留buffering参数已被彻底移除(它自 3.0 起即被忽略并弃用),如需控制底层缓冲请改传已打开的文件对象。 - 多段流读取:
'r'模式下,输入文件可以是多个压缩流的拼接体,BZ2File会透明地依次解压。 - 接口面:
BZ2File实现 io.BufferedIOBase 规定的全部成员,但不含detach()与truncate();支持逐行/逐块迭代与with语句(自 Python 3.1 起支持with)。自 Python 3.10 起文档明确说明:与gzip/lzma中的同类对象一致,BZ2File在多个线程同时读写时并非线程安全。
此外 BZ2File 还提供以下专属方法与属性:
| 成员 | 语义 |
|---|---|
peek([n]) |
返回缓冲数据但不推进文件位置;除非到达 EOF,至少返回一个字节,返回的确切字节数未定义。注意:调用它虽不改变 BZ2File 自身位置,但可能改变底层文件对象的位置(3.3+) |
fileno() |
返回底层文件的文件描述符(3.3+) |
readable() / writable() |
报告是否以读/写模式打开(3.3+) |
seekable() |
报告文件是否支持定位 |
read1(size=-1) |
尽量只发起一次底层读操作来读取至多 size 个未压缩字节;size 为负时读取约一个缓冲区大小的数据;EOF 时返回 b''(3.3+) |
readinto(b) |
读取字节写入 b,返回读取字节数(EOF 为 0)(3.3+) |
mode |
'rb'(读)或 'wb'(写)(3.13+) |
name |
bzip2 文件名,等价于底层文件对象的 name 属性(3.13+) |
从源码看 BZ2File 的实现骨架
BZ2File 的读路径与写路径在 Lib/bz2.py 中分工非常清晰:
- 读模式(
_MODE_READ):构造一个DecompressReader(来自内部模块 Lib/compression/_common/_streams.py,为 gzip/bz2/lzma 等压缩流共享),把"解压器 API"适配成RawIOBase的读取 API,再外包一层io.BufferedReader提供带缓冲读取(Lib/bz2.py)。 - 写模式(
_MODE_WRITE):每个写模式对应创建一个BZ2Compressor(compresslevel);write()先把数据喂给压缩器、立即把产出的压缩块写入底层文件,并累计未压缩字节位置_pos(Lib/bz2.py);直到close()时才调用self._compressor.flush()冲刷内部缓冲并结束压缩流(Lib/bz2.py)。因此文档也提示:磁盘文件内容可能要等close()后才完整反映所写数据。 - 读模式支持多段流与回退定位(seek)的关键在
DecompressReader.read():每当self._decompressor.eof为真,就用unused_data或再从底层文件读取一块,然后新建一个解压器继续处理下一段流(Lib/compression/_common/_streams.py);对不构成合法压缩流的尾部垃圾数据,则通过trailing_error=OSError忽略并结束。底层文件支持seek时,BZ2File.seek()才能工作,且 CPython 文档明确提醒该定位是模拟实现,某些参数组合下可能"极其缓慢"。
增量(解)压缩:面向流式数据
BZ2Compressor:分块喂入、统一冲刷
class bz2.BZ2Compressor(compresslevel=9)
它允许把数据分多次送入压缩器,每次 compress(data) 返回尽可能产出的一段压缩字节,也可能返回空字节串;所有数据送完后,必须调用 flush() 结束压缩过程并取回内部缓冲中剩余的压缩数据。flush() 之后不可再使用该压缩器对象。
文档给出的经典流式压缩示例非常直观:用生成器产生大量重复字节块(b"z"),逐块交给压缩器,最后 flush:
import bz2
def gen_data(chunks=10, chunksize=1000):
"""Yield incremental blocks of chunksize bytes."""
for _ in range(chunks):
yield b"z" * chunksize
comp = bz2.BZ2Compressor()
out = b""
for chunk in gen_data():
# Provide data to the compressor object
out = out + comp.compress(chunk)
# Finish the compression process. Call this once you have
# finished providing data to the compressor.
out = out + comp.flush()
该示例特意使用"非常不随机"的数据流来演示:随机数据往往压缩率很差,而有规律、重复的数据通常能获得很高的压缩率——这也是选择压缩算法时必须考虑的输入特征。
BZ2Decompressor:配合 eof/unused_data/needs_input 精细控制
class bz2.BZ2Decompressor()
它的 decompress(data, max_length=-1) 接收 bytes-like object 并返回解压出的 bytes;部分 data 可能暂存在内部缓冲中留待后续调用处理,每次返回的数据应与前几次调用的输出拼接。
max_length 参数(Python 3.5 加入)用于限制单次返回的最大字节数:
- 若
max_length为非负数,则最多返回max_length字节; - 当达到该上限且仍有后续输出可产生时,
needs_input属性会被置为False,此时下一次调用可传入b''继续索取更多输出; - 若全部输入都被解压返回(无论是因为不足
max_length还是max_length为负),needs_input会被置为True(3.5+)。
需要留意 BZ2Decompressor 的两个重要约束:
- 不透明处理多段流:与
decompress()函数和BZ2File不同,BZ2Decompressor不会自动衔接多个压缩流。要解压多段流输入,必须为每一段流各新建一个解压器。 - 流尾后的行为:到达流尾后再尝试解压会抛出
EOFError;流尾之后发现的多余数据会被忽略,并保存在unused_data属性中(该属性在到达流尾前访问时值为b'')。eof属性则用于判断是否已读到流结束标记(3.3+)。
一个典型的手动流式解压(配合 max_length 与 needs_input)可以这样写:
import bz2
compressed = bz2.compress(b"some payload data")
decomp = bz2.BZ2Decompressor()
results = []
pos = 0
while True:
chunk = compressed[pos:pos + 5] # 故意用小块输入模拟流式场景
pos += 5
out = decomp.decompress(chunk, max_length=8)
results.append(out)
if decomp.eof:
break
if not chunk:
# 无新输入但解压器仍能产出数据
continue
data = b"".join(results)
一次性(解)压缩:最简洁的内存块处理
当数据已完整存在于内存中时,直接使用两个纯函数即可:
bz2.compress(data, compresslevel=9) # 压缩,compresslevel 取 1~9,默认 9
bz2.decompress(data) # 解压
其中 data 均为 bytes-like object。compress() 的实现在 Lib/bz2.py 中非常直白:新建 BZ2Compressor,一次送入数据后立即 flush() 并拼接结果。
decompress() 自 Python 3.3 起支持多段流输入:如果 data 是多个压缩流的拼接,它会全部解压。实现细节(Lib/bz2.py)值得玩味:它用 while 循环反复新建 BZ2Decompressor,解压一段后通过 decomp.eof 与 decomp.unused_data 判定流边界并衔接下一段;若首段即非法流则原样抛出异常,若后续段是非法数据则静默丢弃并正常返回已有结果。
综合示例:压缩比、文件往返与文本读写
压缩比验证(round-trip)
文档给出的往返示例可以直接运行验证:
>>> import bz2
>>> data = b"""\
... Donec rhoncus quis sapien sit amet molestie. Fusce scelerisque vel augue
... nec ullamcorper. Nam rutrum pretium placerat. Aliquam vel tristique lorem,
... sit amet cursus ante. In interdum laoreet mi, sit amet ultrices purus
... pulvinar a. Nam gravida euismod magna, non varius justo tincidunt feugiat.
... Aliquam pharetra lacus non risus vehicula rutrum. Maecenas aliquam leo
... felis. Pellentesque semper nunc sit amet nibh ullamcorper, ac elementum
... dolor luctus. Curabitur lacinia mi ornare consectetur vestibulum."""
>>> c = bz2.compress(data)
>>> len(data) / len(c) # 数据压缩比
1.513595166163142
>>> d = bz2.decompress(c)
>>> data == d # 往返后与原始对象相等
True
压缩比取决于内容本身,上述英文段落文本典型地落在 1.5 倍上下。
二进制文件的写入与读取
>>> import bz2
>>> data = b"""\ # 同上文 data
... """
>>> with bz2.open("myfile.bz2", "wb") as f:
... # Write compressed data to file
... unused = f.write(data)
...
>>> with bz2.open("myfile.bz2", "rb") as f:
... # Decompress data from file
... content = f.read()
...
>>> content == data # 往返校验
True
写入后可用系统 bzip2/bunzip2 命令行工具互相验证——bz2 产出的就是标准 bzip2 流。由于算法本体来自 libbzip2(见 Modules/_bz2module.c 的 <bzlib.h>),文件格式与命令行工具完全互通。
追加写与排他创建
'a'/'ab'追加模式会在已存在的.bz2文件末尾追加一段新的独立压缩流(读取时可被整体还原);'x'/'xb'排他模式在目标文件已存在时抛FileExistsError,可安全用于"不覆盖已有产物"的场景。
OpenTest 中对 'x' 模式的断言正是如此:先创建成功,再次打开即触发 FileExistsError(见 Lib/test/test_bz2.py)。
源码与测试:到哪里继续深入
本主题在 CPython 仓库内的关键落点如下,便于读者按需深挖:
- 纯 Python 封装层:Lib/bz2.py —— 含
BZ2File/open/compress/decompress的全部实现; - C 加速层:Modules/_bz2module.c —— 封装 libbzip2,提供
BZ2Compressor/BZ2Decompressor类型,内部用pycore_blocks_output_buffer.h管理输出缓冲,并以PyMutex(见结构体BZ2Compressor的成员)保护并发访问; - 共享流抽象:Lib/compression/_common/_streams.py ——
BaseStream(模式检查与读写守卫)与DecompressReader(多段流衔接、回退定位)同时服务于 gzip/lzma/bz2 等压缩模块; - 单元测试:Lib/test/test_bz2.py —— 按功能划分为
BZ2FileTest、BZ2CompressorTest、BZ2DecompressorTest、CompressDecompressTest、OpenTest等测试类,覆盖二进制/文本各模式、排他创建、文件对象包装、max_length解压缓冲、压缩级别边界等行为,是理解语义边界的权威样例; - 构建配置:Modules/Setup.stdlib.in 中的
@MODULE__BZ2_TRUE@_bz2 _bz2module.c表明_bz2作为可配置扩展模块编译(注释同时说明它需要链接-lbz2),缺失时即为文档所标注的"可选模块"场景。
结语:如何为你的场景选择接口
| 你的需求 | 推荐 API |
|---|---|
读写磁盘 .bz2 文件,且只需简单调用 |
bz2.open()(默认 'rb',文本数据用 'rt'/'wt' + encoding) |
需要精细控制字节级读写、peek、逐行迭代、甚至 seek |
bz2.BZ2File |
| 数据分块到达、内存受限,需管道式压缩 | bz2.BZ2Compressor + flush() |
| 流式解压且需控制单次输出大小、感知流尾 | bz2.BZ2Decompressor + eof/needs_input/unused_data/max_length |
| 整块数据在内存中快速压缩/解压 | bz2.compress() / bz2.decompress()(后者自动处理多段流) |
在动手前还有两个高频注意点:其一,BZ2Decompressor 不透明处理多段压缩流,遇到拼接流务必逐段新建解压器;其二,压缩级别只对写模式有意义且范围固定为 1~9,并需结合实际数据的可压缩性(随机数据压缩收益极低)来权衡压缩比与耗时。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00