python之Struct解析二进制数据

#python之解析二进制数据

struct模块中最重要的三个函数是pack(), unpack(), calcsize()

struct模块中的函数

函数 return explain
pack(fmt,v1,v2…) string 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流),并将该字符串返回.
pack_into(fmt,buffer,offset,v1,v2…) None 按照给定的格式(fmt),将数据转换成字符串(字节流),并将字节流写入以offset开始的buffer中.(buffer为可写的缓冲区,可用array模块)
unpack(fmt,v1,v2…..) tuple 按照给定的格式(fmt)解析字节流,并返回解析出来的tuple
pack_from(fmt,buffer,offset) tuple 按照给定的格式(fmt)解析以offset开始的缓冲区,并返回解析结果
calcsize(fmt) size of fmt 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式

格式化字符串

当打包或者解包的时,需要按照特定的方式来打包或者解包.该方式就是格式化字符串,它指定了数据类型,除此之外,还有用于控制字节顺序、大小和对齐方式的特殊字符.

对齐方式

为了同c中的结构体交换数据,还要考虑c或c++编译器使用了字节对齐,通常是以4个字节为单位的32位系统,故而struct根据本地机器字节顺序转换.可以用格式中的第一个字符来改变对齐方式.定义如下

Character Byte order Size Alignment
@(默认) 本机(native) 本机(native) 本机,凑够4字节
= 本机(native) 标准(standard) none,按原字节数
< 小端(little-endian) 标准(standard) none,按原字节数
> 大端(big-endian) 标准(standard) none,按原字节数
! network(=big-endian) 标准(standard) none,按原字节数

格式符

格式符 C语言类型 Python类型 Standard size
x pad byte(填充字节) no value
c char string of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I(大写的i) unsigned int integer 4
l(小写的L) long integer 4
L unsigned long long 4
q long long long 8
Q unsigned long long long 8
f float float 4
d double float 8
s char[] string 1
p char[] string 1
P void * long

注- -!

  1. _Bool在C99中定义,如果没有这个类型,则将这个类型视为char,一个字节;
  2. q和Q只适用于64位机器;
  3. 每个格式前可以有一个数字,表示这个类型的个数,如s格式表示一定长度的字符串,4s表示长度为4的字符串;4i表示四个int; 但是p表示的是pascal字符串
  4. P用来转换一个指针,其长度和机器字长相关;
  5. f和d的长度和机器字长相关;

实例代码

以下代码以person的属性进行二进制文件的写入和读取,person包含姓名,年龄,性别,身高,体重,并赋予力量,智力,体力,敏捷,精神五个特征值,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from collections import OrderedDict
import struct
'''
二进制文件的写入和读取
'''


def read_file(file_path):
person = []
file = open(file_path, "rb")
name = file.read(8)
name = struct.unpack('<8s', name)[0]
male = file.read(1)
male = struct.unpack('<b', male)[0]
age = file.read(2)
age = struct.unpack('<H', age)[0]
height = file.read(4)
height = int.from_bytes(height, byteorder='little')
# height = struct.unpack('<I', height)[0]
weight = file.read(4)
weight = struct.unpack('<f', weight)[0]
feature_size = file.read(4)
feature_size = int.from_bytes(feature_size, byteorder='little')
features = []
for i in range(feature_size//4):
feature_value = file.read(4)
feature_value = struct.unpack('<f', feature_value)[0]
features.append(feature_value)
person.append({
"name": name,
"male": male,
"age": age,
"height": height,
"weight": weight,
"feature_size": feature_size,
"features": features,
})
return person


def write_file(file_path, person, features):
file = open(file_path, "wb")
# file.seek(0, 2)
# 把字符串的地方转为字节类型,还要先转成utf-8的编码(否则报错string argument without an encoding)
name = struct.pack('<8s', person['name'].encode('utf-8'))
file.write(name)
male = struct.pack('<B', person['male'])
file.write(male)
age = struct.pack('<H', person['age'])
file.write(age)
height = struct.pack('<I', person['height'])
file.write(height)
weight = struct.pack('<f', person['weight'])
file.write(weight)
features_size = struct.pack('<I', 5*4)
file.write(features_size)
for key, value in features.items():
feature_value = struct.pack('<f', value)
file.write(feature_value)
file.close()


def main():
person = OrderedDict()
person.update({'name': 'Jame'})
person.update({'male': True})
person.update({'age': 25})
person.update({'height': 178})
person.update({'weight': 64.0})
features = {'Strength': 54.0, # 力量
'Intelligence': 78.0, # 智力
'Constitution': 32.0, # 体力
'Dexterity': 78.0, # 敏捷
'Mentality': 53.0 # 精神
}
file_path = "./person_info.pkg"
write_file(file_path, person, features)
person_info = read_file(file_path)
print(person_info)


if __name__ == '__main__':
main()

附录

注意:二进制文件处理时会碰到的问题

我们使用处理二进制文件时,需要用如下方法

binfile=open(filepath,’rb’) 读二进制文件

binfile=open(filepath,’wb’) 写二进制文件

那么和binfile=open(filepath,’r’)的结果到底有何不同呢?

不同之处有两个地方:

第一,使用’r’的时候如果碰到’0x1A’,就会视为文件结束,这就是EOF。使用’rb’则不存在这个问题。即,如果你用二进制写入再用文本读出的话,如果其中存在’0X1A’,就只会读出文件的一部分。使用’rb’的时候会一直读到文件末尾。

第二,对于字符串x=’abc\ndef’,我们可用len(x)得到它的长度为7,\n我们称之为换行符,实际上是’0X0A’。当我们用’w’即文本方式写的时候,在windows平台上会自动将’0X0A’变成两个字符’0X0D’,’0X0A’,即文件长度实际上变成8.。当用’r’文本方式读取时,又自动的转换成原来的换行符。如果换成’wb’二进制方式来写的话,则会保持一个字符不变,读取时也是原样读取。所以如果用文本方式写入,用二进制方式读取的话,就要考虑这多出的一个字节了。’0X0D’又称回车符。Linux下不会变。因为linux只使用’0X0A’来表示换行。

参考

Python 中 struct 模块的用法

Python学习笔记 –struct模板

Python 中的 pack 和 unpack