《Python Cookbook(第3版)中文版》——6.11 读写二进制结构的数组

简介:

本节书摘来自异步社区《Python Cookbook(第3版)中文版》一书中的第6章,第6.11节,作者[美]David Beazley , Brian K.Jones,陈舸 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

6.11 读写二进制结构的数组

6.11.1 问题

我们想将数据编码为统一结构的二进制数组,然后将这些数据读写到Python元组中去。

6.11.2 解决方案

要同二进制数据打交道的话,我们可以使用struct模块。下面的示例将一列Python元组写入到一个二进制文件中,通过struct模块将每个元组编码为一个结构。

from struct import Struct
def write_records(records, format, f):
    '''
    Write a sequence of tuples to a binary file of structures.
    '''
    record_struct = Struct(format)
    for r in records:
        f.write(record_struct.pack(*r))

# Example
if __name__ == '__main__':
    records = [ (1, 2.3, 4.5),
                (6, 7.8, 9.0),
                (12, 13.4, 56.7) ]

with open('data.b', 'wb') as f:
     write_records(records, '<idd', f)

如果要将这个文件重新读取为一列Python元组的话,有好几种方法可以实现。首先,如果打算按块以增量式的方式读取文件的话,可以按照下面的示例来实现:

from struct import Struct
def read_records(format, f):
    record_struct = Struct(format)
    chunks = iter(lambda: f.read(record_struct.size), b'')
    return (record_struct.unpack(chunk) for chunk in chunks)

# Example
if __name__ == '__main__':
    with open('data.b','rb') as f:
        for rec in read_records('<idd', f):
            # Process rec
...

如果只想用一个read()调用将文件全部读取到一个字节串中,然后再一块一块的做转换,那么可以编写如下的代码:

from struct import Struct
def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack_from(data, offset)
            for offset in range(0, len(data), record_struct.size))
# Example
if __name__ == '__main__':
    with open('data.b', 'rb') as f:
        data = f.read()
    for rec in unpack_records('<idd', data):
        # Process rec
        ...

在这两种情况下得到的结果都是一个可迭代对象,它能够产生出之前保存在文件中的那些元组。

6.11.3 讨论

对于那些必须对二进制数据编码和解码的程序,我们常会用到struct模块。要定义一个新的结构,只要简单地创建一个Struct实例即可:

# Little endian 32-bit integer, two double precision floats
record_struct = Struct('<idd')

结构总是通过一组结构化代码来定义,比如i、d、f等(参见Python的文档http://docs.python.org/3/library/struct.html )。这些代码同特定的二进制数据相对应,比如32位整数、64位浮点数、32位浮点数等。而第一个字符<指定了字节序。在这个例子中表示为“小端序”。将字符修改为>就表示为大端序,或者用!来表示网络字节序。

得到的Struct实例有着多种属性和方法,它们可用来操纵那种类型的结构。size属性包含了以字节为单位的结构体大小,这对于I/O操作来说是很有用的。pack()和unpack()方法是用来打包和解包数据的。示例如下:

>>> from struct import Struct
>>> record_struct = Struct('<idd')
>>> record_struct.size
20
>>> record_struct.pack(1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> record_struct.unpack(_)
(1, 2.0, 3.0)
>>>

有时候我们会发现pack()和unpack()会以模块级函数(module-level functions)的形式调用,就像下面的示例这样:

>>> import struct
>>> struct.pack('<idd', 1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> struct.unpack('<idd', _)
(1, 2.0, 3.0)
>>>

这么做行的通,但是比起创建一个单独的Struct实例来说还是显得不那么优雅,尤其是如果相同的结构会在代码中多处出现时。通过创建一个Struct实例,我们只用指定一次格式化代码,所有有用的操作都被漂亮地归组到了一起(通过实例方法来调用)。如果需要同结构打交道的话,这么做肯定会使得代码更容易维护(因为只需要修改一处即可)。

用来读取二进制结构的代码中涉及一些有趣而且优雅的编程惯用法(programming idioms)。在函数read_records()中,我们用iter()来创建一个迭代器,使其返回固定大小的数据块(参见5.8节)。这个迭代器会重复调用一个用户提供的可调用对象(即,lambda: f.read(record_struct.size))直到它返回一个指定值为止(即,b''),此时迭代过程结束。示例如下:

>>> f = open('data.b', 'rb')
>>> chunks = iter(lambda: f.read(20), b'')
>>> chunks
<callable_iterator object at 0x10069e6d0>
>>> for chk in chunks:
...      print(chk)
...
b'\x01\x00\x00\x00ffffff\x02@\x00\x00\x00\x00\x00\x00\x12@'
b'\x06\x00\x00\x00333333\x1f@\x00\x00\x00\x00\x00\x00"@'
b'\x0c\x00\x00\x00\xcd\xcc\xcc\xcc\xcc\xcc*@\x9a\x99\x99\x99\x99YL@'
>>>

创建一个可迭代对象的原因之一在于这么做允许我们通过一个生成器表达式来创建records记录,就像解决方案中展示的那样。如果不采用这种方式,那么代码看起来就会像这样:

def read_records(format, f):
    record_struct = Struct(format)
    while True:
        chk = f.read(record_struct.size)
        if chk == b'':
            break
        yield record_struct.unpack(chk)
    return records

在函数unpack_records()中我们采用了另一种方法。这里使用的unpack_from()方法对于从大型的二进制数组中提取出二进制数据是非常有用的,因为它不会创建任何临时对象或者执行内存拷贝动作。我们只需提供一个字节串(或者任意的数组),再加上一个字节偏移量,它就能直接从那个位置上将字段解包出来。

如果用的是unpack()而不是unpack_from(),那么需要修改代码,创建许多小的切片对象并且还要计算偏移量。示例如下:

def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack(data[offset:offset + record_struct.size])
            for offset in range(0, len(data), record_struct.size))

这个版本的实现除了读取数据变得更加复杂之外,还需要完成许多工作,因为它得计算很多偏移量,拷贝数据,创建小的切片对象。如果打算从已读取的大型字节串中解包出许多结构的话,那么unpack_from()是更加优雅的方案。

我们可能会想在解包记录时利用collections模块中的namedtuple对象。这么做允许我们在返回的元组上设定属性名。示例如下:

from collections import namedtuple
Record = namedtuple('Record', ['kind','x','y'])
with open('data.p', 'rb') as f:
    records = (Record(*r) for r in read_records('<idd', f))
for r in records:
    print(r.kind, r.x, r.y)

如果正在编写一个需要同大量的二进制数据打交道的程序,最好使用像numpy这样的库。比如,与其将二进制数据读取到元组列表中,不如直接将数据读入到结构化的数组中,就像这样:

>>> import numpy as np
>>> f = open('data.b', 'rb')
>>> records = np.fromfile(f, dtype='<i,<d,<d')
>>> records
array([(1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7)],
      dtype=[('f0', '<i4'), ('f1', '<f8'), ('f2', '<f8')])
>>> records[0]
(1, 2.3, 4.5)
>>> records[1]
(6, 7.8, 9.0)
>>>

最后但同样重要的是,如果面对的任务是从某种已知的文件结构中读取二进制数据(例如图像格式、shapefile、HDF5等),请先检查是否已有相应的Python模块可用。没必的话就别去重复发明轮子了。

相关文章
|
4天前
|
存储 测试技术 数据库连接
Python代码结构
【7月更文挑战第16天】Python代码结构
12 2
|
9天前
|
存储 算法 Python
Python图论实战:从零基础到精通DFS与BFS遍历,轻松玩转复杂网络结构
【7月更文挑战第11天】图论在数据科学中扮演关键角色,用于解决复杂网络问题。Python因其易用性和库支持成为实现图算法的首选。本文通过问答形式介绍DFS和BFS,图是节点和边的数据结构,遍历用于搜索和分析。Python中图可表示为邻接表,DFS用递归遍历,BFS借助队列。DFS适用于深度探索,BFS则用于最短路径。提供的代码示例帮助理解如何在Python中应用这两种遍历算法。开始探索图论,解锁更多技术可能!
23 6
|
8天前
|
算法 定位技术 Python
震惊!Python 图结构竟然可以这样玩?DFS&BFS 遍历技巧大公开
【7月更文挑战第12天】在Python中,图数据结构通过邻接表实现,如`Graph`类所示。深度优先搜索(DFS)和广度优先搜索(BFS)是图遍历的关键算法。DFS递归遍历从起点开始的分支,常用于路径查找和连通性检查;BFS使用队列,适用于找最短路径。
15 3
|
6天前
|
存储 对象存储 Python
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
`openpyxl`是一个用于读写Excel 2010 xlsx/xlsm/xltx/xltm文件的Python库。它不需要Microsoft Excel,也不需要.NET或COM组件。
|
8天前
|
存储 缓存 Python
python的结构
【7月更文挑战第12天】python的结构
6 2
|
9天前
|
算法 Python
深度挖掘Python图结构:DFS与BFS遍历的艺术,让复杂问题迎刃而解
【7月更文挑战第11天】在数据结构与算法中,图的遍历如DFS和BFS是解决复杂问题的关键。DFS深入探索直至无路可走,回溯找其他路径,适合找任意解;BFS则逐层扩展,常用于找最短路径。在迷宫问题中,BFS确保找到最短路径,DFS则可能不是最短。Python实现展示了两种方法如何在图(迷宫)中寻找从起点到终点的路径。
|
24天前
|
JSON 安全 数据格式
Python文件操作宝典:一步步教你玩转文件读写
Python文件操作宝典:一步步教你玩转文件读写
|
6天前
|
资源调度 计算机视觉 Python
`scipy.ndimage`是SciPy库中的一个子模块,它提供了许多用于处理n维数组(通常是图像)的函数。
`scipy.ndimage`是SciPy库中的一个子模块,它提供了许多用于处理n维数组(通常是图像)的函数。
|
13天前
|
监控 测试技术 Python
探索Python魅力:利用闭包与装饰器优化代码结构
【7月更文挑战第7天】Python中的闭包和装饰器是强大工具。闭包是能记住外部作用域变量的内部函数,常用于动态函数创建和数据封装。装饰器是接收函数并返回新函数的函数,用于在不修改原代码的情况下扩展功能,如日志或性能监控。通过示例展示了如何使用它们优化代码结构和提升效率。掌握这两者,能写出更优雅高效的Python代码。
|
15天前
|
数据采集 数据挖掘 大数据
Pandas是Python数据分析的核心库,基于NumPy,提供DataFrame结构处理结构化数据
【7月更文挑战第5天】Pandas是Python数据分析的核心库,基于NumPy,提供DataFrame结构处理结构化数据。它支持缺失值处理(dropna()、fillna())、异常值检测(Z-Score、IQR法)和重复值管理(duplicated()、drop_duplicates())。此外,数据转换包括类型转换(astype())、数据标准化(Min-Max、Z-Score)以及类别编码(get_dummies())。这些功能使得Pandas成为大数据预处理的强大工具。