Python 内置模块 struct
struct 模块是 Python 的内置模块,用于在 Python 值和用字节表示的 C 结构体之间进行转换。它主要用于处理二进制数据的打包和解包操作,在网络编程、文件格式处理和与 C 语言交互时非常有用。
基本概念
在了解 struct 模块之前,我们先来理解为什么需要它。
为什么需要 struct 模块?
在 Python 中,我们通常处理的是高级数据类型,如整数、浮点数、字符串等。但在某些场景下,我们需要与底层的二进制数据打交道:
# 普通的 Python 数据
number = 42
text = "hello"
print(f"Python 中的数字: {number}")
print(f"Python 中的文本: {text}")
# 但在网络传输或文件存储时,这些数据需要转换为字节
# 这就是 struct 模块的作用
# 前面章节我们说的将字符 encode 和 decode 这只是针对字符简单的操作,其他数据类型还需要 struct 模块的支持
什么是二进制数据?
计算机内部所有数据都以二进制(0和1)形式存储。让我们看一个简单的例子:
import struct
# Python 整数 42
number = 42
print(f"Python 整数: {number}")
# 转换为二进制字节形式
binary_data = struct.pack('i', number)
print(f"二进制形式: {binary_data}")
print(f"十六进制显示: {binary_data.hex()}")
# 再转换回 Python 整数
restored_number = struct.unpack('i', binary_data)[0]
print(f"恢复的整数: {restored_number}")
实际应用场景举例
想象你要通过网络发送一条消息,包含用户ID(整数)和用户名(字符串):
import struct
# 要发送的数据
user_id = 1001
username = "Alice"
print(f"原始数据 - ID: {user_id}, 用户名: {username}")
# 方法1:直接发送(不推荐,因为接收方难以解析)
simple_data = str(user_id) + username
print(f"简单拼接: '{simple_data}'") # 问题:如何分离ID和用户名?
# 方法2:使用 struct 打包(推荐)
# 格式:4字节整数 + 10字节字符串
packed_data = struct.pack('i 10s', user_id, username.encode())
print(f"struct 打包: {packed_data}")
print(f"十六进制: {packed_data.hex()}")
# 接收方可以精确解包
unpacked_id, unpacked_name = struct.unpack('i 10s', packed_data)
print(f"解包结果 - ID: {unpacked_id}, 用户名: {unpacked_name.decode().rstrip('\x00')}")
struct 模块通过格式字符串来定义数据的布局方式。格式字符串描述了数据的类型、大小和字节序(端序)。
字节序和对齐
第一个字符可以指定字节序和对齐方式:
@
:本机字节序,本机对齐(默认)=
:本机字节序,标准对齐<
:小端序,标准对齐>
:大端序,标准对齐!
:网络字节序(大端序),标准对齐
import struct
# 演示不同字节序的影响
data = 0x12345678
# 大端序(网络字节序)
big_endian = struct.pack('>I', data)
print(f"大端序: {big_endian.hex()}") # 12345678
# 小端序
little_endian = struct.pack('<I', data)
print(f"小端序: {little_endian.hex()}") # 78563412
# 本机字节序
native_endian = struct.pack('I', data)
print(f"本机字节序: {native_endian.hex()}")
格式字符
常用的格式字符包括:
x
:填充字节c
:char(1字节)b
:signed char(1字节)B
:unsigned char(1字节)h
:short(2字节)H
:unsigned short(2字节)i
:int(4字节)I
:unsigned int(4字节)l
:long(4字节)L
:unsigned long(4字节)q
:long long(8字节)Q
:unsigned long long(8字节)f
:float(4字节)d
:double(8字节)s
:char[](字符串)p
:pascal string
import struct
# 不同数据类型的打包示例
byte_data = struct.pack('b', -128) # 有符号字节
print(f"字节: {byte_data}")
short_data = struct.pack('h', 12345) # 短整型
print(f"短整型: {short_data}")
int_data = struct.pack('i', 1234567890) # 整型
print(f"整型: {int_data}")
float_data = struct.pack('f', 3.14159) # 浮点数
print(f"浮点数: {float_data}")
# 字符串需要指定长度
string_data = struct.pack('10s', b'hello') # 10字节字符串
print(f"字符串: {string_data}")
基本操作
pack() - 打包数据
pack() 函数将 Python 值转换为字节串。
import struct
# 单个值打包
packed_int = struct.pack('i', 42)
print(f"打包的整数: {packed_int}")
# 多个值打包
packed_multiple = struct.pack('i f 4s', 100, 3.14, b'test')
print(f"多值打包: {packed_multiple}")
# 数组打包(使用重复计数)
packed_array = struct.pack('3i', 1, 2, 3)
print(f"数组打包: {packed_array}")
# 使用填充字节
packed_with_padding = struct.pack('i x 4s', 42, b'data') # x 表示一个填充字节
print(f"带填充打包: {packed_with_padding}")
unpack() - 解包数据
unpack() 函数将字节串转换回 Python 值。
import struct
# 准备一些二进制数据
binary_data = struct.pack('i f 4s', 100, 3.14, b'test')
# 解包数据
unpacked = struct.unpack('i f 4s', binary_data)
print(f"解包结果: {unpacked}")
print(f"整数: {unpacked[0]}, 浮点数: {unpacked[1]}, 字符串: {unpacked[2]}")
# 解包单个值
single_data = struct.pack('d', 2.718281828)
unpacked_single = struct.unpack('d', single_data)[0]
print(f"解包单个双精度浮点数: {unpacked_single}")
calcsize() - 计算大小
calcsize() 函数返回格式字符串对应的字节数。
import struct
# 计算不同格式的大小
print(f"int 大小: {struct.calcsize('i')} 字节")
print(f"double 大小: {struct.calcsize('d')} 字节")
print(f"复合格式大小: {struct.calcsize('i f 10s')} 字节")
# 计算数组大小
print(f"5个整数大小: {struct.calcsize('5i')} 字节")
# 不同字节序的大小可能不同
print(f"本机对齐 int: {struct.calcsize('@i')} 字节")
print(f"标准对齐 int: {struct.calcsize('=i')} 字节")
Struct 类
对于重复使用相同格式的情况,可以创建 Struct 对象来提高效率。
import struct
# 创建 Struct 对象
s = struct.Struct('i f 8s')
print(f"格式大小: {s.size} 字节")
# 使用 Struct 对象打包
data1 = s.pack(42, 3.14, b'python')
data2 = s.pack(100, 2.71, b'struct')
print(f"数据1: {data1}")
print(f"数据2: {data2}")
# 使用 Struct 对象解包
unpacked1 = s.unpack(data1)
unpacked2 = s.unpack(data2)
print(f"解包1: {unpacked1}")
print(f"解包2: {unpacked2}")
处理文件头信息
struct 模块常用于解析二进制文件格式。以下示例演示如何处理简单的文件头。
import struct
# 模拟创建一个包含文件头信息的二进制数据
def create_file_header():
# 文件头格式:魔数(4字节), 版本(2字节), 文件大小(4字节), 标志(1字节)
magic_number = 0x12345678
version = 1
file_size = 1024
flags = 0x01
header = struct.pack('>I H I B', magic_number, version, file_size, flags)
return header
# 解析文件头
def parse_file_header(header_data):
# 解包文件头
magic, version, size, flags = struct.unpack('>I H I B', header_data)
print(f"魔数: 0x{magic:08x}")
print(f"版本: {version}")
print(f"文件大小: {size} 字节")
print(f"标志: 0x{flags:02x}")
return magic, version, size, flags
# 演示使用
header_data = create_file_header()
print(f"文件头二进制数据: {header_data.hex()}")
print("解析结果:")
parse_file_header(header_data)
处理网络数据包
在网络编程中,struct 常用于构造和解析数据包。
import struct
import socket
# 创建一个简单的网络数据包
def create_packet(packet_type, sequence, data):
# 包格式:类型(1字节), 序号(4字节), 数据长度(2字节), 数据
data_length = len(data)
# 使用网络字节序
header = struct.pack('!B I H', packet_type, sequence, data_length)
packet = header + data
return packet
# 解析网络数据包
def parse_packet(packet_data):
# 先解析包头(7字节)
header_size = struct.calcsize('!B I H')
if len(packet_data) < header_size:
print("数据包太短")
return None
packet_type, sequence, data_length = struct.unpack('!B I H', packet_data[:header_size])
# 提取数据部分
data = packet_data[header_size:header_size + data_length]
print(f"包类型: {packet_type}")
print(f"序列号: {sequence}")
print(f"数据长度: {data_length}")
print(f"数据: {data}")
return packet_type, sequence, data
# 演示使用
test_data = b"Hello, Network!"
packet = create_packet(1, 12345, test_data)
print(f"创建的数据包: {packet}")
print("解析数据包:")
parse_packet(packet)
注意事项
- 字符串处理:字符串格式需要明确指定长度,超出长度的部分会被截断,不足的部分会用零填充。
- 字节序一致性:打包和解包时必须使用相同的字节序格式。
- 数据类型范围:确保数据值在对应类型的有效范围内,超出范围会导致错误或数据丢失。
- 内存对齐:不同的对齐方式可能导致结构体大小不同,需要根据实际需求选择。