Шаг 111.
Python: сборник рецептов.
Кодирование и обработка данных. Чтение и запись бинарных массивов структур

    На этом шаге мы рассмотрим способы реализации этой задачи.

Задача

    Вы хотите прочесть или записать данные, закодированные как бинарный массив из единообразных структур, в кортежи Python.

Решение

    Чтобы работать с бинарными данными, используйте модуль struct. Вот пример кода, который записывает список кортежей Python в бинарный файл, кодируя каждый кортеж в структуру с помощью модуля struct:

from struct import Struct


def write_records(records, format, f):
    '''
    Записывает последовательность кортежей в бинарный файл структур.
    '''
    record_struct = Struct(format) 
    for r in records:
        f.write(record_struct.pack(*r))


#  Пример
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)

    Есть несколько подходов к обратному превращению этого файла в список кортежей. Во-первых, если вы читаете файл пошагово кусочками (chunks), то можете написать такой код:

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)


#  Пример
if __name__ == '__main__':
    with open('data.b', 'rb') as f:
    for rec in read_records('<idd', f):
        # Обработка записи
        .   .   .

    Если вы хотите прочесть файл целиком в байтовую строку за один проход и преобразовывать его кусочек за кусочком, вы можете сделать это так:

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, ien(data), record_struct.size))


# Пример
if __name__ == '__main__':
    with open('data.b', 'rb') as f: 
        data = f.read()
        for rec in unpack_records('<idd', data): 
            # Обработка записи
            .   .   .

    В обоих случаях результатом будет итерируемый объект, который производит кортежи, которые были сохранены в файле при его создании.

Обсуждение

    В программах, которые должны кодировать и декодировать бинарные данные, обычно используют модуль struct. Чтобы объявить новую структуру, просто создайте экземпляр Struct, как показано ниже:

#  32-битное целое число (little endian), два числа с плавающей точкой
#  двойной точности 
record_struct = Struct('<idd')

    Структуры всегда определяются путем использования набора кодов структур, таких как i, d, f и т. д.


См. документацию Python https://docs.python.org/3/library/struct.html.

    Эти коды соответствуют определенным бинарным типам данных, таким как 32-битные целые числа, 64-битные числа с плавающей точкой, 32-битные числа с плавающей точкой и т. д. Символ < в качестве первого символа определяет порядок следования байтов. В этом примере он задает порядок байт от младшего к старшему (little endian). Замените символ на >, чтобы задать порядок байт от старшего к младшему (big endian), или на ! для сетевого порядка байтов.

    Полученный экземпляр Struct имеет различные атрибуты и методы для манипулирования структурами этого типа. Атрибут size содержит размер структуры в байтах, что полезно для операций ввода-вывода. Методы 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() вызываются, как функции уровня модуля:

>>> 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 форматирующий код определяется только единожды, и все полезные операции прекрасным образом сгруппированы вместе. Это точно увеличит легкость поддерживания вашего кода (ведь вам придется вносить изменения только в одном месте).

    Код для чтения бинарных структур использует несколько интересных и элегантных идиом программирования. В функции read_records() функция iter() используется для создания итератора, который возвращает кусочки (chunks) фиксированного размера (см. 86 шаг). Этот итератор раз за разом вызывает переданный пользователем вызываемый объект (в данном случае 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@'
>>>

    Смысл использования итератора в том, что он позволяет записям создаваться с помощью генератора (generator comprehension), как показано в примере. Если бы вы не использовали это решение, ваш код мог бы выглядеть так:

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(). Это полезный метод для извлечения бинарных данных из более крупного бинарного массива, потому что он делает это без создания временных объектов или копий в памяти. Вы просто передаете ему байтовую строку (или любой массив) вместе с байтовым сдвигом (offset), и он распакует поля прямо из этого места.

    Если вы использовали 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() будет более элегантным решением.

    Распаковка записей - одна из областей, где могут найти применение объекты namedtuple из модуля collections. Они позволят вам установить имена атрибутов на возвращаемые кортежи. Например:

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)
>>>

    И последнее: если вы столкнулись с задачей чтения бинарных данных в каком-то известном формате (например, форматах растровых или векторных изображений, HDF5 и т. д.), проверьте, нет ли в Python модуля для работы с ними. Не стоит изобретать велосипед, если можно обойтись без этого.

    На следующем шаге мы рассмотрим чтение вложенных и различных по размеру бинарных структур.




Предыдущий шаг Содержание Следующий шаг