在用 Python 处理底层通信、文件格式解析或者和 C 程序交换数据时,struct 模块几乎是绕不开的工具。它负责把 Python 里的基础类型转换成连续的字节串,也能把字节串还原成变量。但不少人在实际使用中遇到过这样一种情况:明明只定义了几个短整型和字符,打包出来的结果却比预期长,或者发到对端设备后被解析成一堆乱码。这类问题通常并不是代码写错,而是没有真正弄清楚 struct 里的字节对齐规则和字节序设定。

字节对齐是怎么悄悄改变数据长度的
struct 模块在默认情况下使用的是本机对齐方式,也就是遵循当前运行环境(通常是 C 编译器)的对齐策略。所谓对齐,是指为了让 CPU 更高效地读取内存,编译器会在结构体成员之间插入一些无意义的填充字节。例如在一个本机格式下,如果先放一个单字节的字符,再放一个四字节的整数,那么这个整数往往不会紧挨着字符存放,而是被移到第四个字节的位置上,中间空出三个字节。
这种行为在纯 Python 环境里很容易被忽略,因为平时我们操作的是高级对象,不需要关心内存地址。但一旦用 struct.pack 把数据变成字节,填充字节就会实实在在出现在结果里。如果不指定对齐模式,同一份格式串在 32 位和 64 位机器上、在不同编译器环境下,可能产生不同长度的字节串,给跨语言通信埋下隐患。
要避免这种不确定性,struct 提供了明确的格式控制符。在格式串最前面加 = 表示使用标准大小但本机对齐,加 < 或 > 表示标准大小加小端或大端且无对齐填充,加 ! 则表示网络序无对齐。只要写清楚这些前缀,就能彻底关掉自动补齐,让每个字段严丝合缝地排列。
import struct
# 本机对齐,可能有填充
native = struct.pack('ci', b'A', 1)
print(len(native)) # 可能是 8
# 标准无对齐小端
std = struct.pack('<ci', b'A', 1)
print(len(std)) # 一定是 5
字节序决定多字节数据怎么摆放
字节序描述的是一个多字节数字在内存里先写高位还是先写低位。常见的有两种:小端序(little-endian)把低位字节放在前面,大端序(big-endian)把高位字节放在前面。x86 架构的电脑基本都是小端,而网络传输协议普遍规定用大端,也就是网络字节序。
如果发送方和接收方字节序不一致又不作转换,解析出来的整数就会完全不对。比如整数 1 在小端下存成 x01x00x00x00,在大端下却是 x00x00x00x01。struct 模块通过格式符前缀来处理这件事:< 强制小端,> 强制大端,! 等同于大端用于网络,= 则跟随本机。不写前缀时默认也是本机序,所以在写通用解析代码时绝不能省略。
下面示例展示同一个数字在不同字节序下打包结果的差异,可以直观看到字节排列变化。实际项目中,和服务端约定好的协议文档通常会写明大小端,我们要做的就是在格式串首字符上严格对应。
import struct
num = 0x01020304
little = struct.pack('<i', num)
big = struct.pack('>i', num)
print(little) # b'x04x03x02x01'
print(big) # b'x01x02x03x04'
跨平台读写实例与避坑建议
假设我们在树莓派(小端 ARM)上采集传感器数据,要通过 TCP 发给云端 Linux 服务器(也是小端),同时存档成文件供 Windows 工具(小端)读取。表面看大家都是小端似乎不用管,但文件还可能被客户用 macOS(也是小端)打开,这时若代码用了本机对齐,不同系统结构体补齐规则若有细微差别就会出错。最稳妥的做法是协议统一写成 < 或 ! 开头,彻底消除对齐和字节序的歧义。
另一个常见坑是混用字符串和数值。struct 对字符串用 s 表示,且长度要前置,如 10s 表示十个字节的字符串。字符串本身不受字节序影响,但如果你在它前面放了多字节整数又忘了关对齐,长度照样会飘。推荐把格式串写成常量集中管理,例如 FMT = '<H10sI',分别代表小端无对齐的短整型、十字节字符串、无符号整型,这样维护和联调都清晰。
最后提醒,用 struct.calcsize 可以在不打包的情况下算出格式串占用字节数,是验证对齐设置是否生效的好办法。如果发现算出来比手算的大,基本就是对齐填充在起作用。结合 unpack 时严格按同一格式串解析,就能做到多端一致,不再被莫名多出来的字节干扰。
import struct FMT = '<H10sI' print(struct.calcsize(FMT)) # 16,无填充 data = struct.pack(FMT, 7, b'sensor_123', 999) magic, name, count = struct.unpack(FMT, data) print(magic, name, count)