Python二进制数据处理

Published on 2017 - 01 - 25

处理文本数据比较晦涩难懂(新旧格式化、正则表达式等),而处理二进制数据就有趣多了。你需要了解像字节序(endianness,电脑处理器是如何将数据组织存储为字节的)以及整数的符号位(sign bit)之类的概念。你可能需要研究二进制文件格式、网络包等内容,从而对其中的数据进行提取甚至修改。

字节和字节数组

Python 3 引入了下面两种使用 8 比特序列存储小整数的方式,每 8 比特可以存储从 0~255 的值:

  • 字节是不可变的,像字节数据组成的元组;
  • 字节数组是可变的,像字节数据组成的列表。

我们的示例从创建列表 blist 开始。接着需使用这个列表创建一个 bytes 类型的变量 the_bytes 以及一个 bytearray 类型的变量 the_byte_array:

>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')

bytes 类型值的表示形式比较特殊:以 b 开头,接着是一个单引号,后面跟着由十六进制数(例如 \x02)或 ASCII 码组成的序列,最后以配对的单引号结束。Python 会将这些十六进制数或者 ASCII 码转换为整数,如果该字节的值为有效 ASCII 编码则会显示 ASCII 字符。

>>> b'\x61'
 b'a'

>>> b'\x01abc\xff'
 b'\x01abc\xff'

下面的例子说明了 bytes 类型的不可变性:

>>> the_bytes[1] = 127
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

但 bytearray 类型的变量是可变的:

>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
bytearray(b'\x01\x7f\x03\xff')

下面两行代码都会创建一个包含 256 个元素的结果,包含 0~255 的所有值:

>>> the_bytes = bytes(range(0, 256))
>>> the_byte_array = bytearray(range(0, 256))

打印 bytes 或 bytearray 数据时,Python 会以 \xxx 的形式表示不可打印的字符,以 ASCII 字符的形式表示可打印的字符(以及一些转义字符,例如 \n 而不是 \x0a)。下面是 the_bytes 的打印结果(手动设置为一行显示 16 个字节):

>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f
\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~\x7f
\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f
\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f
\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf
\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf
\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef
\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

看起来可能有些困惑,毕竟上面输出的数据是字节(小整数)而不是字符。

使用struct转换二进制数据

如你所见,Python 中有许多文本处理工具(模块、函数等),然而处理二进制数据的工具则要少得多。标准库里有一个 struct 模块,专门用于处理类似 C 和 C++ 中结构体的数据。你可以使用 struct 模块的功能将二进制数据转换为 Python 中的数据结构。

以一个 PNG 文件(一种常见的图片格式,其他图片格式还有 GIF、JPEG 等)为例看看 struct 是如何工作的。我们来编写一个小程序,从 PNG 文件中获得图片的宽度和高度信息。使用 O'Reilly 的经典标志:一只睁大了眼睛的眼镜猴,见图 1。

[图 1:O'Reilly 的标志眼镜猴]

你可以在Wikipedia 上获取这张图片的 PNG 文件。这里我编写了一个简单的小程序将它的数据以字节形式打印出来,然后将起始的 30 字节数据存入 Pythonbytes 型变量 data 中,如下所示。方便起见,你只需复制这部分数据即可。(PNG 格式规定了图片的宽度和高度信息存储在初始 24 字节中,因此不需要其他的额外数据。)

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
...     width, height = struct.unpack('>LL', data[16:24])
...     print('Valid PNG, width', width, 'height', height)
... else:
...     print('Not a valid PNG')
...
Valid PNG, width 154 height 141

以上代码说明:

  • data 包含了 PNG 文件的前 30 字节内容,为了书的排版,我将这 30 字节数据放到了两行字节串中,并用 + 和续行符(\)将它们连接起来;
  • valid_png_header 包含 8 字节序列,它标志着这是一个有效的 PNG 格式文件;
  • width 值位于第 16~20 字节,height 值则位于第 21~24 字节。

上面代码中的 >LL 是一个格式串,它用于指导 unpack() 正确解读字节序列并将它们组装成 Python 中的数据类型。可以将它分解成下面几个基本格式标志:

  • > 用于指明整数是以大端(big-endian)方案存储的;
  • 每个 L 代表一个 4 字节的无符号长(unsigned long)整数。

你可以直接获取 4 字节数据:

>>> data[16:20]
b'\x00\x00\x00\x9a'
>>> data[20:24]0x9a
b'\x00\x00\x00\x8d'

大端方案将高字节放在左侧。由于宽度和高度都小于 255,因此它们存储在每一个 4 字节序列的最后一字节中。不难验证,上面的十六进制数转换为十进制后与我们预期的数值(图片的宽和高)一致:

>>> 0x9a
154
>>> 0x8d
141

如果想要执行上述过程的逆过程,将 Python 数据转换为字节,可以使用 struct pack() 函数:

>>> import struct
>>> struct.pack('>L', 154)
b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
b'\x00\x00\x00\x8d'

表 5 和表 6 列出了 pack() 和 unpack() 使用的一些格式标识符。

首先是字节序标识符。

[表5:字节序标识符]

标识符 字节序
< 小端方案
> 大端方案

[表6:格式标识符]

标识符 描述 字节
x 跳过一个字节 1
b 有符号字节 1
B 无符号字节 1
h 有符号短整数 2
H 无符号短整数 2
i 有符号整数 4
I 无符号整数 4
l 有符号长整数 4
L 无符号长整数 4
Q 无符号 long long 型整数 8
f 单精度浮点数 4
d 双精度浮点数 8
p 数量和字符 1 + 数量
s 字符 数量

类型标识符紧跟在字节序标识符的后面。任何标识符的前面都可以添加数字用于指定需要匹配的数量,例如 5B 代表 BBBBB。

可以使用数量前缀改写 >LL:

>>> struct.unpack('>2L', data[16:24])
(154, 141)

之前的例子中使用了切片 data[16:24] 直接获取所需的特定字节,也可以使用 x 标识符来跳过不需要的字节:

>>> struct.unpack('>16x2L6x', data)
(154, 141)

上面格式串的含义如下:

  • 使用大端方案(>)
  • 跳过 16 个字节(16x)
  • 读取 8 字节内容——两个无符号长整数(2L)
  • 跳过最后 6 个字节(6x)

其他二进制数据工具

一些第三方开源包提供了下面这些更加直观地定义和提取二进制数据的方法:

接下来的几个例子需要提前安装 construct 包,只需执行下面这行代码即可:

$ pip install construct

下面的例子展示了如何使用 construct 从之前的 data 中提取 PNG 图片的尺寸:

>>> from construct import Struct, Magic, UBInt32, Const, String
>>> # 基于https://github.com/construct上的代码修改而来
>>> fmt = Struct('png',
...     Magic(b'\x89PNG\r\n\x1a\n'),
...     UBInt32('length'),
...     Const(String('type', 4), b'IHDR'),
...     UBInt32('width'),
...     UBInt32('height')
...     )
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> result = fmt.parse(data)
>>> print(result)
Container:
    length = 13
    type = b'IHDR'
    width = 154
    height = 141
>>> print(result.width, result.height)
154, 141

使用binascii()转换字节/字符串

标准 binascii 模块提供了在二进制数据和多种字符串表示(十六进制、六十四进制、uuencoded,等等)之间转换的函数。例如,下面的小例子将 8- 字节的 PNG 头打印为十六进制值的形式,而不是 Python 默认的打印 bytes 型变量的方式:混合使用 ASCII 和转义的 \x xx。

>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
b'89504e470d0a1a0a'

反过来转换也可以:

>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
b'\x89PNG\r\n\x1a\n'

位运算符

Python 提供了和 C 语言中类似的比特级运算符。表 7 列出了这些位运算符并附上了整数 a(十进制 5,二进制 0b0101)和 b(十进制 1,二进制 0b0001)的运算示例。

[表7:比特级整数运算符]

运算符 描述 示例 十进制结果 二进制结果
& a & b 1 0b0001
` ` `a b`
^ 异或 a ^ b 4 0b0100
~ 翻转 ~a -6 取决于 int 类型的大小
<< 左位移 a << 1 10 0b1010
>> 右位移 a >> 1 2 0b0010

& 返回两个运算数中相同的比特。| 返回两个运算数中任意一者有效的比特。^ 返回仅在一个运算数中有效的比特。~ 将所有比特翻转。现代计算机都使用二进制补码(two's complement)进行运算,其中整数的最高位定义为符号位(0 为正,1 为负),因此翻转操作会改变运算数的符号。<< 和 >> 仅仅将比特向左或向右移动,左位移操作相当于将数字乘以 2,右位移操作相当于将数字除以 2。

参考文档