2.29. Struct и двоичные данные

Модуль struct упаковывает значения Python в фиксированную двоичную структуру и распаковывает байты обратно в значения Python. Обращайтесь к нему при работе с двоичным форматом файла, сетевым протоколом или устройством, обменивающимся записями фиксированного размера.

Большинство случаев покрывают две функции:

  • struct.pack() – принимает значения Python и строку формата, возвращает объект bytes с точно заданной структурой.

  • struct.unpack() – принимает строку формата и объект bytes, возвращает кортеж значений Python.

2.29.1. Строки формата

Строка формата перечисляет по одному коду на каждое поле записи. Коды описывают как размер, так и интерпретацию каждого поля.

Тип int в Python не имеет фиксированного размера – он растёт, чтобы вместить любое присвоенное значение. Двоичные форматы имеют фиксированные размеры: каждое целочисленное поле использует согласованное число байтов. struct выполняет преобразование между неограниченными целыми Python и этими представлениями фиксированного размера.

Разрядность целого числа – это количество используемых им битов. Один байт равен восьми битам. Код в нижнем регистре обозначает знаковый вариант; код в верхнем регистре – беззнаковый (только неотрицательные значения):

  • b / B8-битное (один байт). -128..127 со знаком, 0..255 без знака.

  • h / H16-битное (два байта). -32768..32767 со знаком, 0..65535 без знака.

  • i / I32-битное (четыре байта). Примерно ±два миллиарда со знаком, четыре миллиарда без знака.

  • q / Q64-битное (восемь байтов). Практически неограниченное для повседневного использования.

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

Остальные распространённые коды предназначены для чисел с плавающей точкой и байтовых строк:

  • f – 32-битное число с плавающей точкой (одинарная точность; около семи десятичных знаков). Обычный float в MicroPython уже имеет этот размер, поэтому упаковка его в f происходит без потерь.

  • d – 64-битное число с плавающей точкой (двойная точность; около пятнадцати десятичных знаков). Упаковка 32-битного float MicroPython в d расширяет его до восьми байтов, но не добавляет точности.

  • s – байтовая строка фиксированной длины, перед которой указывается количество (8s для восьмибайтового поля).

2.29.2. Порядок байтов

Многобайтовое целое число может храниться в памяти двумя способами. Число 0x12345678 в 32-битном поле размещается так:

  • Little-endian (от младшего) – сначала наименее значащий байт: 78 56 34 12.

  • Big-endian (от старшего) – сначала наиболее значащий байт: 12 34 56 78.

Оба кодируют одно и то же значение; они расходятся лишь в том, на каком конце поля находится младший байт. Файл, записанный одной системой, читается с искажениями на другой, если порядок байтов не совпадает.

Ведущий символ строки формата выбирает порядок:

  • < – little-endian. Распространён на x86 и ARM.

  • > – big-endian. Распространён в сетевых протоколах.

  • ! – сетевой порядок, эквивалентен >.

Без ведущего символа используются собственный порядок байтов и собственное выравнивание платформы; явное указание < или > устраняет эту неоднозначность и обычно является тем, что нужно при чтении файла или обмене данными с другой машиной.

Примечание

OpenMV Cam использует порядок little-endian – такой же, как у её хост-ПК. Используйте < в строках формата для локальных файлов камеры и для двоичных данных, передаваемых на настольный компьютер или с него. Используйте > (или !) для сетевых протоколов и для любого формата, спецификация которого требует big-endian.

Шесть байтов, расположенных в ряд, где первые два байта сгруппированы как поле "H" (16-битное беззнаковое), а следующие четыре -- как поле "I" (32-битное беззнаковое), каждое помечено своим порядком байтов little-endian.

"<HI" упаковывает 16-битное значение, за которым следует 32-битное значение, в шесть байтов в порядке little-endian.

2.29.3. Упаковка

import struct

blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))

Вывод:

b'@\x01@B\x0f\x00' 6

Формат <HI создаёт шесть байтов: два для поля H и четыре для поля I, все в порядке little-endian. Передавайте ровно столько значений, сколько ожидает формат – несоответствие вызывает struct.error.

2.29.4. Распаковка

width, count = struct.unpack("<HI", blob)
print(width, count)

Вывод:

320 1000000

struct.unpack() всегда возвращает кортеж, даже когда формат описывает единственное поле. Для удобочитаемости распаковывайте его в той же строке.

2.29.5. Байтовые строки фиксированной длины

Код s читает или записывает фрагмент байтов дословно. Количество указывается перед s4s означает «четыре байта, рассматриваемые как единая байтовая строка». Это обычный способ внедрить в запись магическое значение, тег фиксированного размера или дополненное до нужной длины поле имени:

header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)

Вывод:

b'OMV0@\x01@B\x0f\x00'

Первые четыре байта – это литеральное магическое значение b"OMV0"; следующие два – поле H (320); последние четыре – поле I (1000000). Распаковка возвращает байты обратно в виде объекта bytes:

magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)

Вывод:

b'OMV0' 320 1000000

Если исходное значение короче объявленного количества, результат дополняется справа байтами \x00; если длиннее, лишние байты незаметно отбрасываются:

struct.pack("4s", b"hi")        # b'hi\x00\x00'
struct.pack("4s", b"toolong")   # b'tool'

Количество – это длина в байтах, а не число символов – s работает с сырыми байтами, поэтому строку UTF-8 с многобайтовыми символами нужно сначала .encode() и посчитать длину в байтах.

2.29.6. Определение размера и частичное чтение

struct.calcsize() возвращает количество байтов, занимаемых строкой формата:

struct.calcsize("<HI")     # 6

При чтении потока записей из файла читайте ровно столько байтов на каждую запись:

record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
    while True:
        chunk = f.read(record_size)
        if len(chunk) < record_size:
            break
        width, count = struct.unpack("<HI", chunk)
        print(width, count)

Неполное чтение в конце файла даёт фрагмент меньше record_size – рассматривайте это как условие конца потока, а не пытайтесь распаковать частичную запись.