3、Python基础回顾:字节与字节数组、struct模块、二进制数据处理、位运算
说实话,很多做上层应用开发的Python程序员,可能几年都碰不到一次二进制数据。但一旦你开始做网络协议解析、文件格式处理、或者像我一样做嵌入式通信协议封装,你就会发现——字节才是计算机最诚实的语言。
这一章,我们就把Python里跟二进制打交道的几个核心工具过一遍。别觉得基础,我当年在解析一个Modbus协议时,就因为搞混了字节序,整整调了两天bug。
3.1 字节与字节数组:内存里的最小单元
Python 3里,bytes和bytearray是处理二进制数据的两个主力。它们的区别很简单:bytes不可变,bytearray可变。你想想看,这跟str和list的关系差不多。
核心区别速览
| 特性 | bytes | bytearray |
|---|---|---|
| 可变性 | 不可变 | 可变 |
| 创建方式 | b"hello" 或 bytes(10) |
bytearray(10) 或 bytearray(b"hello") |
| 索引取值 | 返回 int (0-255) | 返回 int (0-255) |
| 切片 | 返回 bytes | 返回 bytearray |
| 典型用途 | 哈希值、固定协议头 | 缓冲区、动态拼包 |
# 创建方式
data1 = b'\x01\x02\x03\x04' # 字面量
data2 = bytes([0x01, 0x02, 0x03]) # 从列表
data3 = bytearray(8) # 8个零字节
# 索引取值——注意返回的是整数
print(data1[0]) # 1,不是 b'\x01'
# 切片
print(data1[1:3]) # b'\x02\x03'
# bytearray 可以原地修改
buf = bytearray(b'\x00\x00\x00\x00')
buf[0] = 0xFF
print(buf.hex()) # 'ff000000'
我的习惯:在协议解析时,如果数据需要频繁修改(比如填充校验和),我优先用 bytearray。如果只是读取协议头,用 bytes 更安全,防止误改。
3.2 struct模块:C语言结构体的Python翻译官
做协议解析时,最常遇到的情况就是:从一段二进制流里,按固定格式提取出多个字段。比如一个网络包,前2字节是长度,接着4字节是序号,后面是负载数据。
手动用切片去拆?能写,但太累了。而且还要处理字节序、对齐等问题。这时候 struct 模块就是你的救星。
3.2.1 打包与解包
struct.pack 把Python值转成字节,struct.unpack 把字节转回Python值。核心是格式字符串,它告诉Python怎么解释这些字节。
import struct
# 打包:将整数和浮点数转为字节
packed = struct.pack('<If', 42, 3.14)
# '<' 小端序,'I' 无符号4字节整数,'f' 4字节浮点数
print(packed.hex()) # 输出类似 '2a000000c3f54840'
# 解包:从字节中提取
unpacked = struct.unpack('<If', packed)
print(unpacked) # (42, 3.140000104904175)
我曾经踩过的坑:浮点数解包后精度有微小误差,这是IEEE 754标准决定的。如果你在金融或计费场景里用 struct 处理浮点,建议改用整数表示(比如分单位),否则对账时你会哭的。
3.2.2 格式字符串速查
| 格式符 | C类型 | Python类型 | 字节数 |
|---|---|---|---|
b |
signed char | int | 1 |
B |
unsigned char | int | 1 |
h |
short | int | 2 |
H |
unsigned short | int | 2 |
i |
int | int | 4 |
I |
unsigned int | int | 4 |
f |
float | float | 4 |
d |
double | float | 8 |
s |
char[] | bytes | 指定长度 |
3.2.3 字节序:大端 vs 小端
这是协议解析里最容易出bug的地方。说白了,就是多字节数据在内存里怎么排列:
- 大端序 (
>):高位字节在前。网络协议几乎都用这个。 - 小端序 (
<):低位字节在前。x86架构的CPU用这个。 - 网络序 (
!):其实就是大端序,!是>的别名。
# 同一个数值,不同字节序
val = 0x12345678
big = struct.pack('>I', val)
little = struct.pack('<I', val)
print(big.hex()) # '12345678'
print(little.hex()) # '78563412'
我的建议:写协议解析代码时,永远显式指定字节序。不要依赖默认的 @(本机字节序),因为你的代码可能从Windows移植到ARM Linux上,默认字节序就变了。
3.3 二进制数据处理:从原始字节到结构化数据
光会 struct 还不够,实际项目中你面对的是变长协议、嵌套结构、位字段。这里我分享一个我在智能家居项目里用过的解析模式。
3.3.1 变长协议解析示例
假设协议格式:[起始符(2B)][长度(2B)][类型(1B)][数据(NB)][校验(1B)]
import struct
def parse_packet(raw: bytes):
"""解析自定义协议包"""
if len(raw) < 6:
raise ValueError("包太短")
# 解析固定头部
head = struct.unpack('<HHB', raw[:5])
start_flag, length, ptype = head
if start_flag != 0xAA55:
raise ValueError("起始符错误")
# 根据长度提取数据
data_start = 5
data_end = data_start + length
payload = raw[data_start:data_end]
# 校验和(简单累加)
checksum = raw[data_end]
calc = sum(raw[:data_end]) & 0xFF
if checksum != calc:
raise ValueError("校验失败")
return {
'type': ptype,
'payload': payload,
'checksum': checksum
}
# 模拟一个包
packet = struct.pack('<HHB', 0xAA55, 4, 0x01)
packet += b'\x01\x02\x03\x04'
packet += bytes([sum(packet) & 0xFF])
result = parse_packet(packet)
print(result)
3.4 位运算:在字节级别操控比特
位运算在协议解析里太常用了。尤其是处理标志位、位字段、掩码操作时,你离不开它。
3.4.1 六种基本位运算
| 运算符 | 名称 | 示例 | 结果 |
|---|---|---|---|
& |
按位与 | 0b1100 & 0b1010 |
0b1000 |
| |
按位或 | 0b1100 | 0b1010 |
0b1110 |
^ |
按位异或 | 0b1100 ^ 0b1010 |
0b0110 |
~ |
按位取反 | ~0b1100 |
-0b1101(注意符号) |
<< |
左移 | 0b0001 << 3 |
0b1000 |
>> |
右移 | 0b1000 >> 2 |
0b0010 |
3.4.2 实战:从字节中提取位字段
很多协议会把多个标志位塞进一个字节里。比如一个状态字节,bit0表示电源状态,bit1-3表示模式,bit4-7保留。
status_byte = 0b10101101
# 提取bit0(电源状态)
power = status_byte & 0x01
print(f"电源: {'开' if power else '关'}")
# 提取bit1-3(模式)
mode = (status_byte >> 1) & 0b111
print(f"模式: {mode}")
# 设置bit4为1(开启某个功能)
status_byte |= (1 << 4)
print(bin(status_byte)) # 0b10111101
# 清除bit5
status_byte &= ~(1 << 5)
print(bin(status_byte)) # 0b10011101
注意:Python的整数是无限精度的,取反操作 ~ 会得到负数。如果你只想对8位取反,记得用 ~x & 0xFF 截断。
3.5 本章知识体系
下面这张图把本章的核心知识点串起来了。你可以把它当作一个思维导图,看看自己哪里还不太熟。
嗯,这一章的内容就到这里。字节、struct、位运算,这三样东西在后续的协议封装解析中会反复出现。你最好在本地把代码敲一遍,尤其是那个变长协议解析的例子——改改参数,看看不同情况下的表现。