2.29. 结构体与二进制数据¶
struct 模块可以将 Python 值打包成固定的二进制布局,并将字节解包回 Python 值。当你处理二进制文件格式、网络协议,或与交换固定大小记录的设备通信时,可以使用它。
两个函数即可涵盖大多数情况:
struct.pack()—— 接收 Python 值和一个格式字符串,返回一个具有精确布局的bytes对象。struct.unpack()—— 接收一个格式字符串和一个bytes对象,返回一个 Python 值组成的元组。
2.29.1. 格式字符串¶
格式字符串为记录中的每个字段列出一个代码。这些代码同时描述了每个字段的大小和解释方式。
Python 的 int 没有固定大小——它会自动增长以容纳你赋予的任何值。而二进制格式确实有固定大小:每个整数字段都使用约定数量的字节。struct 在无界的 Python 整数与这些固定大小的表示之间进行转换。
整数的位宽是它所使用的比特数。一个字节是八比特。小写代码是有符号变体;大写代码是无符号变体(仅非负值):
b/B—— 8 位(一个字节)。有符号为-128..127,无符号为0..255。h/H—— 16 位(两个字节)。有符号为-32768..32767,无符号为0..65535。i/I—— 32 位(四个字节)。有符号约为 ±二十亿,无符号约为四十亿。q/Q—— 64 位(八个字节)。对于日常使用来说实际上是无界的。
选择一个能够轻松覆盖你预期范围的位宽。打包一个超出声明范围的值,根据构建版本的不同,要么会静默地回绕,要么会引发 struct.error。
其余常用代码用于浮点数和字节串:
2.29.2. 字节序¶
一个多字节整数可以用两种方式存储在内存中。数字 0x12345678 在一个 32 位字段中的布局如下:
小端序 —— 最低有效字节在前:
78 56 34 12。大端序 —— 最高有效字节在前:
12 34 56 78。
两者编码的是同一个值;它们只是在字段的哪一端是低字节这一点上有分歧。如果字节序不匹配,一个系统写入的文件在另一个系统读取时会变成乱码。
格式字符串的首字符决定了字节序:
<—— 小端序。在 x86 和 ARM 上常见。>—— 大端序。在网络协议中常见。!—— 网络序,等同于>。
如果没有首字符,则使用本机字节序和本机对齐方式;显式设置 < 或 > 可以消除这种歧义,在读取文件或与另一台机器通信时通常正是你想要的。
备注
OpenMV Cam 是小端序的——与其主机 PC 相同。对于摄像头本地文件以及往返于桌面的二进制数据,在格式字符串中使用 <。对于网络协议以及任何规范要求大端序的格式,使用 >(或 !)。
"<HI" 将一个 16 位值后跟一个 32 位值打包成六个小端序字节。¶
2.29.3. 打包¶
import struct
blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))
输出:
b'@\x01@B\x0f\x00' 6
<HI 格式生成六个字节:H 字段两个,I 字段四个,全部为小端序。传入的值数量必须与格式所期望的完全一致——不匹配会引发 struct.error。
2.29.4. 解包¶
width, count = struct.unpack("<HI", blob)
print(width, count)
输出:
320 1000000
struct.unpack() 总是返回一个元组,即使格式只描述了一个字段。为了可读性,可以在同一行将其解包。
2.29.5. 固定长度字节串¶
s 代码原样读取或写入一段字节。计数放在 s 之前——4s 表示"作为单个字节串处理的四个字节"。这是在记录中嵌入魔数、固定大小标签或填充名称字段的常用方式:
header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)
输出:
b'OMV0@\x01@B\x0f\x00'
前四个字节是字面魔数 b"OMV0";接下来的两个是 H 字段(320);最后四个是 I 字段(1000000)。解包会将字节作为 bytes 对象返回:
magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)
输出:
b'OMV0' 320 1000000
如果源值比声明的计数短,结果会在右侧用 \x00 填充;如果更长,多余的字节会被静默丢弃:
struct.pack("4s", b"hi") # b'hi\x00\x00'
struct.pack("4s", b"toolong") # b'tool'
该计数是字节长度,而不是字符数——s 处理的是原始字节,因此一个含多字节字符的 UTF-8 字符串需要先 .encode() 并按字节计数。
2.29.6. 计算大小与部分读取¶
struct.calcsize() 返回一个格式字符串所消耗的字节数:
struct.calcsize("<HI") # 6
当从文件中读取一连串记录时,每条记录恰好读取那么多字节:
record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
while True:
chunk = f.read(record_size)
if len(chunk) < record_size:
break
width, count = struct.unpack("<HI", chunk)
print(width, count)
文件末尾的短读取会产生一个小于 record_size 的数据块——应将其视为流结束条件,而不是试图解包一条不完整的记录。