在系统编程、网络协议解析以及文件格式处理中,经常会遇到需要直接操作字节的情况。Python提供了内置的字节类型以及标准库模块,可以让开发者以可控的方式读写二进制数据,而不必关心底层内存布局的细节。

字节类型基础
Python中与二进制数据相关的核心类型是bytes和bytearray。前者是不可变字节序列,适合表示固定的数据块;后者是可变字节序列,适合在解析过程中逐步修改内容。两者都支持索引和切片,但索引返回的是整数而非字符。
例如,构造一个包含三个字节的对象可以使用字面量或调用构造函数。字节值范围必须是0到255之间的整数,否则会抛出错误。理解这一点对于后续使用struct模块非常重要,因为所有打包操作最终都会生成符合该范围的字节序列。
# 不可变字节对象 b = bytes([0x01, 0x02, 0x03]) print(b[0]) # 输出 1 # 可变字节数组 ba = bytearray(b) ba[0] = 0xFF print(ba) # 输出 bytearray(b'xffx02x03')
使用struct模块打包与解包
struct模块是解决二进制处理问题的关键工具。它定义了格式字符串,用来描述数据在字节中的排列方式。通过pack函数可以把Python值转换为字节,通过unpack可以把字节还原为Python值。格式字符包括整数、浮点、字符等类型,并可用前缀指定字节序和对齐方式。
如果不指定字节序,默认使用本机顺序和本机对齐,这在跨平台时容易出问题。通常建议显式使用<表示小端、>表示大端。下面的例子展示如何把一个无符号短整数和浮点数以小端方式写入字节,再读取出来。
import struct # 小端格式:H表示无符号短整数,f表示浮点数 fmt = '<Hf' data = struct.pack(fmt, 1024, 3.14) print(data) # 输出对应字节 # 解包还原 num, val = struct.unpack(fmt, data) print(num, val)
处理字符串与定长字段
二进制协议中的文本常以固定长度存储,不足部分用空字节填充。struct支持用s加数字表示定长字节串,例如10s代表长度为10的字节串。注意它不会自动编码字符串,需要手动用encode和decode转换。
下面示例将一个名字写入并读出。若原始字符串超过长度会被截断,不足则用空字节补齐,读取后需去除多余空字符以保证内容正确。
import struct
name = 'alice'.encode('utf-8')
fmt = '<10s'
packed = struct.pack(fmt, name)
print(packed)
# 解包并去除空字节
raw = struct.unpack(fmt, packed)[0]
print(raw.rstrip(b'x00').decode('utf-8'))
手动解析的误区与对比
有些开发者倾向于用移位和按位与来提取字段,这种方法在简单场景下可行,但面对嵌套结构、不同字节序时会迅速变得难以维护。比如用(b[0] << 8) | b[1]解析大端短整数,若实际数据是小端就会得到错误结果。
使用struct不仅减少代码量,也把字节序和类型长度集中声明在格式串中,降低出错概率。在性能敏感场合,可配合struct.Struct缓存格式对象来减少重复解析开销,这是手动移位难以直观实现的优化。
import struct
# 缓存格式对象提升重复调用效率
parser = struct.Struct('<IH')
binary = parser.pack(1, 2)
a, b = parser.unpack(binary)
print(a, b)
实际应用场景建议
在处理自定义网络报文或解析旧版文件格式时,先画字段布局表,再用struct格式串一一映射,能显著提升代码可读性。对于变长内容,通常把长度字段放在头部,先读长度再读数据体,避免一次性读取造成阻塞或越界。
当数据量极大时,可结合memoryview避免复制字节,再用struct对切片解包,既节省内存又保持清晰逻辑。掌握这些组合用法,Python处理二进制数据就能做到既稳妥又高效。
import struct
from io import BytesIO
buf = BytesIO()
buf.write(struct.pack('<I', 3))
for i in range(3):
buf.write(struct.pack('<H', i * 10))
buf.seek(4)
for _ in range(3):
print(struct.unpack('<H', buf.read(2))[0])