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