Python 二进制数据处理教程:struct 模块详解

在处理网络协议解析、嵌入式设备通信、二进制文件读取这类场景时,Python 内置的 bytes/bytearray 虽然能存储数据,但直接操作十六进制字符太繁琐。这时候,struct 模块就是你的瑞士军刀——它用人类可读的「格式字符串」,一键完成 Python 原生类型和二进制数据的双向转换。


1. 先搞懂:Python 里的二进制存储

先铺垫基础,Python 里专门存二进制的是两个类型:

  • bytes:不可变字节序列,用 b'' 字面量创建
  • bytearray:可变字节序列,可修改单个字节
# 常见的 bytes 写法
b1 = b'hello'  # 直接用ASCII字符串
b2 = b'\x00\x9c@c'  # 手动写十六进制字节
print(b2)  # 输出结果完全一致

2. struct 模块核心

2.1 两个最常用的函数

不需要记复杂的函数名,核心就两个:

函数作用
struct.pack把 Python 数据打包成二进制 bytes
struct.unpack把二进制 bytes/bytearray 解包成 Python 元组
import struct

# 第一步:导入模块(别漏!)

2.2 核心规则:格式字符串

格式字符串是 struct 的「指挥棒」,由可选的字节顺序+对齐方式 + 必选的类型说明符 组成。

字节顺序/对齐字符

强烈建议不要省略这部分!否则会依赖运行平台的原生规则,导致代码在 Windows/macOS/Linux 上表现不同。

字符规则说明
<小端序(低字节在前,通用PC)
>大端序(高字节在前,网络/嵌入式常用)
!网络序(等价于 >,专门给协议用)
@原生顺序+对齐(慎用,平台依赖
=原生顺序+标准大小无对齐(少用)

常用数据类型说明符

记住这些高频类型,90%的场景都能覆盖:

格式对应C语言类型转换为Python类型字节大小
x填充字节(跳过)1
bsigned charint1
Bunsigned charint1
hshortint2
Hunsigned shortint2
iintint4
Iunsigned intint4
qlong longint8
Qunsigned long longint8
ffloatfloat4
ddoublefloat8
schar[]bytes前缀数字决定长度
pPascal字符串bytes前缀数字决定长度(首字节存长度)

3. 上手试试:基础实操

3.1 单个整数的打包解包

拿大端序(通用协议/存储)举例子:

# 打包:把整数10240099转成4字节大端无符号整数
n = 10240099
packed = struct.pack('>I', n)
print(packed)  # 输出 b'\x00\x9c@c'

# 解包:必须用【完全相同的格式字符串】
# unpack返回元组,取第0个元素就是整数
unpacked = struct.unpack('>I', packed)[0]
print(unpacked)  # 输出 10240099

3.2 混合多种数据类型

格式字符串可以按顺序拼接多个说明符,对应pack/unpack的多个参数/返回值:

# 打包:大端序,4字节无符号整数 + 2字节无符号短整数
mixed_packed = struct.pack('>IH', 4042322160, 32896)
print(mixed_packed)  # 输出 b'\xf0\xf0\xf0\xf0\x80\x80'

# 解包:得到长度为2的元组
mixed_unpacked = struct.unpack('>IH', mixed_packed)
print(mixed_unpacked)  # 输出 (4042322160, 32896)

4. 实战练手:解析BMP文件头

光说不练假把式,我们写个小工具读取本地(或模拟的)BMP图片的宽、高、位深!

import struct
import base64

def analyze_bmp_header(data: bytes) -> dict | None:
    """分析BMP文件前30字节的标准头"""
    if len(data) < 30:
        raise ValueError("数据太短,无法解析BMP头")
    
    # BMP头固定格式:<(小端,Windows文件通用)+ 2s I H H I I I I H H
    # 对应顺序:文件标识BM、文件大小、保留位1、保留位2、数据偏移、头大小、宽、高、色彩平面数、位深
    header = struct.unpack('<2sIHHIIIIHH', data[:30])
    
    # 先验证是不是合法的BMP(前2字节必须是b'BM')
    if header[0] != b'BM':
        return None
    
    return {
        'type': header[0].decode('ascii'),
        'size': header[1],
        'width': header[6],
        'height': header[7],
        'bits_per_pixel': header[9]
    }

def bmp_info(data: bytes) -> dict | None:
    """简化版:只返回常用的宽、高、位深"""
    full_info = analyze_bmp_header(data)
    if not full_info:
        return None
    return {
        'width': full_info['width'],
        'height': full_info['height'],
        'color': full_info['bits_per_pixel']
    }

if __name__ == '__main__':
    # 这里用base64编码的模拟BMP头,避免依赖本地文件
    bmp_base64 = (
        'Qk1oAgAAAAAAADYAAAAoAAAAHAAAAAoAAAABABAAAAAAADICAAASCwAAEgsAA'
        'AAAAAAAAAAA/3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3/'
        '/f/9//3//f/9//3//f/9/AHwAfAB8AHwAfAB8AHwAfP9//3//fwB8AHwAfAB8/3//f/9/A'
        'HwAfAB8AHz/f/9//3//f/9//38AfAB8AHwAfAB8AHwAfAB8AHz/f/9//38AfAB8/3//f/9'
        '//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9//3//fwB8AHz/f'
        '/9//3//f/9/AHwAfP9//3//f/9//3//f/9//38AfAB8AHwAfAB8AHwAfP9//3//f/9/AHw'
        'AfP9//3//f/9//38AfAB8/3//f/9//3//f/9//3//fwB8AHwAfAB8AHwAfAB8/3//f/9//'
        '38AfAB8/3//f/9//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9'
        '//3//fwB8AHz/f/9/AHz/f/9/AHwAfP9//38AfP9//3//f/9/AHwAfAB8AHwAfAB8AHwAf'
        'AB8/3//f/9/AHwAfP9//38AfAB8AHwAfAB8AHwAfAB8/3//f/9//38AfAB8AHwAfAB8AHw'
        'AfAB8/3//f/9/AHwAfAB8AHz/fwB8AHwAfAB8AHwAfAB8AHz/f/9//3//f/9//3//f/9//'
        '3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//38AAA=='
    )
    bmp_data = base64.b64decode(bmp_base64)
    
    # 运行并打印
    info = bmp_info(bmp_data)
    print(f"模拟BMP图片信息:{info}")
    
    # 验证结果(确保我们的解析正确)
    assert info['width'] == 28
    assert info['height'] == 10
    assert info['color'] == 16
    print("✅ BMP解析测试通过!")

5. 避坑指南与最佳实践

  1. 必须指定字节顺序:永远别用默认的 @,否则跨平台必出bug
  2. 处理异常:解包时容易遇到长度不匹配的问题,记得捕获 struct.error
  3. 大数据用memoryview:处理几十MB以上的二进制数据时,用 memoryview 切片可以减少内存复制
  4. 查官方文档:冷门格式(比如Pascal字符串 p)直接翻 Python struct官方文档

6. 还能做什么?

除了解析图片,struct 模块的常用场景还有:

  • 解析 TCP/UDP 网络包头(比如 IPv4、DNS)
  • 读取/写入 Excel 的旧版二进制格式
  • 与 C/C++ 编写的动态库交互(传递二进制数据)
  • 控制 Arduino、树莓派等嵌入式设备的通信协议

通过今天的教程,你应该能快速上手 struct 处理常见的二进制数据了!如果遇到更复杂的嵌套结构,可以结合 struct.calcsize (计算格式字符串的字节数)分步解析,或者找现成的第三方库(比如 construct)辅助。