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 本章知识体系

下面这张图把本章的核心知识点串起来了。你可以把它当作一个思维导图,看看自己哪里还不太熟。

Python二进制数据处理知识体系 二进制数据处理 字节与字节数组 bytes:不可变,哈希安全 bytearray:可变,适合缓冲区 索引返回int,切片返回原类型 struct模块 pack:Python值 → 字节 unpack:字节 → Python值 格式字符串:< > ! 字节序 I/H/B/f/d/s 等格式符 二进制数据处理 变长协议解析 嵌套结构提取 校验和计算 异常处理与边界检查 位运算 & | ^ ~ << >> 掩码提取:val & mask 位设置:val |= (1 << n) 位清除:val &= ~(1 << n) 注意Python无限精度取反

嗯,这一章的内容就到这里。字节、struct、位运算,这三样东西在后续的协议封装解析中会反复出现。你最好在本地把代码敲一遍,尤其是那个变长协议解析的例子——改改参数,看看不同情况下的表现。