2.29. Struct и двоичные данные¶
Модуль struct упаковывает значения Python в фиксированную двоичную структуру и распаковывает байты обратно в значения Python. Обращайтесь к нему при работе с двоичным форматом файла, сетевым протоколом или устройством, обменивающимся записями фиксированного размера.
Большинство случаев покрывают две функции:
struct.pack()– принимает значения Python и строку формата, возвращает объектbytesс точно заданной структурой.struct.unpack()– принимает строку формата и объектbytes, возвращает кортеж значений Python.
2.29.1. Строки формата¶
Строка формата перечисляет по одному коду на каждое поле записи. Коды описывают как размер, так и интерпретацию каждого поля.
Тип int в Python не имеет фиксированного размера – он растёт, чтобы вместить любое присвоенное значение. Двоичные форматы имеют фиксированные размеры: каждое целочисленное поле использует согласованное число байтов. struct выполняет преобразование между неограниченными целыми Python и этими представлениями фиксированного размера.
Разрядность целого числа – это количество используемых им битов. Один байт равен восьми битам. Код в нижнем регистре обозначает знаковый вариант; код в верхнем регистре – беззнаковый (только неотрицательные значения):
b/B– 8-битное (один байт).-128..127со знаком,0..255без знака.h/H– 16-битное (два байта).-32768..32767со знаком,0..65535без знака.i/I– 32-битное (четыре байта). Примерно ±два миллиарда со знаком, четыре миллиарда без знака.q/Q– 64-битное (восемь байтов). Практически неограниченное для повседневного использования.
Выбирайте разрядность, с запасом покрывающую ожидаемый диапазон. Упаковка значения за пределами объявленного диапазона либо незаметно переполняется по кругу, либо вызывает struct.error, в зависимости от сборки.
Остальные распространённые коды предназначены для чисел с плавающей точкой и байтовых строк:
f– 32-битное число с плавающей точкой (одинарная точность; около семи десятичных знаков). Обычныйfloatв MicroPython уже имеет этот размер, поэтому упаковка его вfпроисходит без потерь.d– 64-битное число с плавающей точкой (двойная точность; около пятнадцати десятичных знаков). Упаковка 32-битногоfloatMicroPython в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.
"<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 читает или записывает фрагмент байтов дословно. Количество указывается перед s – 4s означает «четыре байта, рассматриваемые как единая байтовая строка». Это обычный способ внедрить в запись магическое значение, тег фиксированного размера или дополненное до нужной длины поле имени:
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 – рассматривайте это как условие конца потока, а не пытайтесь распаковать частичную запись.