Skip to main content

Python 常用内置模块

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)

注意事项

  1. 字符串处理:字符串格式需要明确指定长度,超出长度的部分会被截断,不足的部分会用零填充。
  2. 字节序一致性:打包和解包时必须使用相同的字节序格式。
  3. 数据类型范围:确保数据值在对应类型的有效范围内,超出范围会导致错误或数据丢失。
  4. 内存对齐:不同的对齐方式可能导致结构体大小不同,需要根据实际需求选择。