Python二进制数据处理

前言

本文所涉及到的代码均基于python 3.x。

str与bytes

在写代码时,我们经常会涉及到字符串的处理,但字符串的编码问题常常令我们很头痛。当然本篇文章不是去说编码的,而是主要讲讲二进制处理,python 3为我们提供了bytes,利用bytes可以一定程度上缓解编码出错的问题,因为bytes是字节序列,无所谓编码。下面先看一个例子。

_str = 'str'
print(type(_str))
# 输出为 <class 'str'>

_bytes = b'bytes'
print(type(_bytes))
# 输出为<class 'bytes'>

看到差别了吧,在定义字符串的时候,字符串引号之前加上一个b那么所定义的就是bytes。
上面的_bytes变量虽然是字节序列,但是print()出来只不过字符串前面加了个b,好像和str没有多大区别,那么我们再看下面这段代码:

_str = '中国'
print(_str)
# 中国

_bytes = '中国'.encode()
print(_bytes)
# b'\xe4\xb8\xad\xe5\x9b\xbd'

这下差别就比较明显了吧,对于中文,str打印出来是它本身,bytes打印出来就是它的16进制编码了。当你在网络上发送一个中文str给别人的时候,如果你使用的编码格式和对方不一致,对方直接读取str可能就是乱码,而bytes就没有这个问题。
上面的代码还涉及到str和bytes的转换,这里我们也说一下。先看代码:

_str = '中国'
print(type(_str))  # <class 'str'>
print(_str)  # 中国

_bytes = _str.encode()
print(type(_bytes))  # <class 'bytes'>
print(_bytes)  # b'\xe4\xb8\xad\xe5\x9b\xbd'

new_str = _bytes.decode()
print(type(new_str))  # <class 'str'>
print(new_str)  # 中国

可以看到,str转bytes通过encode()方法,而bytes转str则通过decode()方法,encode()方法默认encoding=‘utf-8’。
另外,str有的方法,bytes大部分也都有,因为这篇文章不是主要讲字符串处理问题的,所以这里就不展开了。

base64模块

现在我们来说一下base64模块。首先,base64是一种用64个字符来表示二进制数据的方法。具体表示方法是:
首先定义出用来表示二进制数据的64个字符:

['a', 'b', ..., '+', '/']  # 省略中间60个字符

然后,将需要处理的二进制数据进行分组,每3个字节一组,也就是3 x 8 = 24 bit。然后再将这24个bit划分为4组,每组6个bit。那么每组的6个bit就可以表示0至63之间的数字,这样我们就可以查一开始定义的64个字符表,来获得二进制数据的字符表示了。
这种情况下,原始二进制数据的长度就增加了,那岂不是增大了数据传输的压力?这里,需要清楚的一点是,base64处理的主要是显示乱码问题。如果我们直接打开一些二进制文件,很多情况下出来的是一堆乱码,而用base64表示之后,我们打开这些数据,就能够清楚的看到里面的内容,比如邮件、图片等,当然它也用于URL、Cookie、网页中传输少量数据。
还有一个问题,如果原始二进制数据的字节数不是3的整数倍怎么办?在这种情况下,base64会在原始二进制数据末尾添加\x00来补齐数据,然后,在编码后的字符串,这些被补齐的地方表示成=,在解码的时候,这些=再被去掉,也能得到正确的原始二进制数据。这些操作在python的base64模块自动完成的,我们实际不用关心。
说了这么多,我们看一下python里怎么使用base64吧。

import base64

_bytes = b'test bytes'

b64 = base64.b64encode(_bytes)
print(b64)  # b'dGVzdCBieXRlcw=='

new_bytes = base64.b64decode(b64)
print(new_bytes)  # b'test bytes'

注意,上面的b'test bytes'是10个字节,经过base64处理后末尾添加了两个=,这样就是12个字节,也就是3的整数倍。
最后,前面有说到过,base64可以用来处理URL,但base64的64个字符里会有+ /,这些符号在URL中是不能直接作为参数的。因此, base64还提供了一种url safe的处理方式,也就是把base64编码后的字符串中的+ /替换成- _。下面看一个例子:

import base64

_bytes = b'\xfd\xcf\xbe'
b64 = base64.b64encode(_bytes)
print(b64)  # b'/c++'

us_b64 = base64.urlsafe_b64encode(_bytes)
print(us_b64)  # b'_c--'

new_bytes = base64.urlsafe_b64decode(us_b64)
print(new_bytes)  # b'\xfd\xcf\xbe'

可以看到,经过urlsafe_b64encode()编码的字符串相比b64encode()编码的字符串确实有所不同。
另外,base64的64个字符的顺序是可以自定义的,这样就可以自定义编码,但一般情况下没有这个必要。

struct模块

最后,我们来说一下struct模块,这也是python中常用的处理二进制数据的模块。
struct模块用来处理的是python数据和表示成python bytes对象的C结构体(struct)之间的转换,应用场景一般是处理文件和网络传输中的二进制数据。
我们还是通过一个例子来了解struct模块的应用吧:

struct s_data {
	unsigned short id;
	unsigned int length;
	char[5] data;
}

假设我们有一个上面这样的C结构体,那么它与python值之间如何转换呢?请看代码:

from struct import Struct

p_id = 0
p_length = 5
p_data = b'hello'

c_struct = Struct('>HI5s')
packed = c_struct.pack(p_id, p_length, p_data)
print(packed)  # b'\x00\x00\x00\x00\x00\x05hello'

unpacked = c_struct.unpack(b'\x00\x00\x00\x00\x00\x05hello')
print(unpacked)  # (0, 5, b'hello')

在上面的代码中,首先我们在python里定义了结构体里的各个成员,然后调用pack()方法把它打包成bytes,这样就可以与C进行数据交换了。又或者下面两行代码里,我们从C收到一串二进制数据,我们可以调用unpack方法把它解析出来(解析出来的数据是放在元组里的)。
在下面这行代码中我们实际定义了C结构体里的数据格式:

c_struct = Struct('>HI5s')

第一个符号>表示数据采用大端方式(big-endian)存储,这个主要是考虑到有的C/C++的编译器使用了字节对齐。这个字符其实是定义了字节序、大小和打包后数据的对齐方式,可以参看下表:

CharacterByte OrderSizeAlignment
@nativenativenative
=nativestandardnone
<little-endianstandardnone
>big-endianstandardnone
!network(=big-endian)standardnone

如果我们在写程序的时候没有把第一个字符定义为上述表格中的字符,那么默认的是@。需要注意的是,当我们想要处理与平台无关的数据格式时,使用size为standard的方式,不要使用native的方式。
>符号后面的几个符号中,H表示C里的unsigned short,I表示C里的unsigned int,s表示C里的char[],5s表示的是5个元素的char数组。下面将C与python的对应类型列出来:

FormatC TypePython TypeStandard size
xpad byteno value
ccharbytes of length 11
bsigned charinteger1
Bunsigned charinteger1
?_Boolbool1
hshortinteger2
Hunsigned shortinteger2
iintinteger4
Iunsigned intinteger4
llonginteger4
Lunsigned longinteger4
qlong longinteger8
Qunsigned long longinteger8
nssize_tinteger
Nsize_tinteger
efloat2
ffloatfloat4
ddoublefloat8
schar[]bytes
pchar[]bytes
Pvoid*integer

需要注意的是,Standard size列有固定值的都是与平台无关的大小,而没有具体的值的,是与平台相关的。
最后,对于上面的代码,其实还可以这样写:

import struct

p_id = 0
p_length = 5
p_data = b'hello'


packed = struct.pack('>HI5s', p_id, p_length, p_data)
print(packed)  # b'\x00\x00\x00\x00\x00\x05hello'

unpacked = struct.unpack('>HI5s', b'\x00\x00\x00\x00\x00\x05hello')
print(unpacked)  # (0, 5, b'hello')

但是我觉得这种写法没有实例方法优雅,特别是在代码中同样的结构出现在多个地方的时候。通过创建一个Struct实例,格式化字符串只会指定一次并且所有的操作被集中处理。这样一来代码维护就变得简单了(因为只需要改变一处代码即可)。

参考链接

廖雪峰 base64 教程
廖雪峰 struct 教程
python3 module struct doc

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页