Метка: ctypes

Пакуем байты на Python: struct

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

Встроенный модуль struct как раз создан для этих целей. В низкоуровневом деле важны детали, а именно размер каждого элемента данных, их порядок в структуре, а также порядок байт для многобайтовых типов данных. Для определения этих деталей модуль struct вводит форматные строки (не путать с str.format, там другой формат).

Начнем с простого примера:

>>> import struct
>>> struct.pack("hhl", 1, 2, 3)
b'\x01\x00\x02\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00'

Здесь происходит вот что. Мы берем три числа: 1, 2, 3 и пакуем их в байты, таким образом, что первое и второе числа трактуются как тип short int (4 байта на моей машине), а последнее, как long int (8 байт на моей машине). Это типы не из Python, а из языка Си. Ознакомьтесь с типами языка Си, если хотите понимать, что они из себя представляют и какой размер в байтах имеют.

Обратная распаковка байт в кортеж значений по заданному формату:

>>> struct.unpack("hhl", b'\x01\x00\x02\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00')
(1, 2, 3)

Форматы запаковки и распаковки должны совпадать, иначе данные будут неправильно интерпретированы или испорчены, или же вообще возникнет ошибка из-за того, что размер данных не подходит под ожидаемый формат (struct.error):

>>> struct.unpack("hhl", b'\x01\x02\x03')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
struct.error: unpack requires a buffer of 16 bytes

Обратите внимание, что я выше писал, что размер элемента «h» – 4 байта именно на моей машине. Может статься так, что на машине с другим процессором, архитектурой или просто с другой версией ОС размер типа будет другой. Для 32 битных систем, это обычно будет 2 байта.

Но, что если данных передаются по сети или через носители информации между системами с разной или неизвестной заранее архитектурой? Конечно, у struct есть средства на такие случаи. Первый символ форматной строки обозначит порядок байт. Обратите внимание на таблицу:

СимволПорядок байтРазмеры типовВыравнивание
@нативныйнативныйнативное
=нативныйстандартныенет
<little-endianстандартныенет
>big-endianстандартныенет
!сетевой
(= big-endian)
стандартныенет

Нативный – значит родной для конкретно вашей машины и системы. По умолчанию порядок байт и размер типов данных как раз нативный (символ @).

Стандартный размер – размер, который фиксирован стандартом и не зависит от текущей платформы. Например, char всегда 1 байт, а int – 4 байта. Если мы планируем распространять запакованные байты, мы должны гарантировать, что размер типов будет всегда стандартный. Для этого подходит любой из символов «=«, «<«, «>«, «!» в начале форматной строки.

Little-endian и big-endian

Little-endian и big-endian – это два основных порядка байт. Представим, что у нас есть короткое целое (short int), и оно занимает два (2) байта. Какой из байтов должен идти сначала, а какой в конце?

В big-endian порядок от старшего байта к младшему. В little-endian порядок от младшего байта к старшему. Как узнать на Python какой порядок байт в системе:

>>> import sys
>>> sys.byteorder
'little'

Давайте наглядно посмотрим как пакуются байты при разных порядках. Для числа 258 в форме short младший байт будет = 2, а старший = 1:

258 = 2*20 + 1*28

>>> struct.pack("<h", 258)  # little-endian
b'\x02\x01'
>>> struct.pack(">h", 258)  # big-endian
b'\x01\x02'

Как видите порядок байт противоположный для разных случаев.

В сетевых протоколах принято использовать big-endian (символ «!» – псевдоним к «>«), а на большинстве современных настольных систем используется little-endian.

Таблица типов данных

Теперь ознакомимся с таблицей типов данных, которая дает соответствие символу форматной строки (код преобразования) с Си-типом данных, Python-типом данных и стандартный размером. Еще раз: стандартный размер будет только, если задан первый символ как «<«, «>«, «!» или «=«. Для «@» или по умолчанию – размер данных определяется текущей системой (платформо-зависимо).

СимволТип в языке СиPython типСтанд. размер
xбайт набивкинет значения
ccharbytes длины 11
bsigned charinteger1
Bunsigned charinteger1
?_Boolbool1
hshortinteger2
Hunsigned shortinteger2
iintinteger4
Iunsigned intinteger4
llonginteger4
Lunsigned longinteger4
qlong longinteger8
Qunsigned long longinteger8
nssize_tintegerзависит
Nsize_tintegerзависит
e«половинный float«float2
ffloatfloat4
ddoublefloat8
schar[]bytesуказывается явно
pchar[] — строка из Паскаляbytesуказывается явно

Коды «e«, «f«, «d» используют бинарный формат IEEE-754.

Код «x» это просто байт набивки. Он не попадает в распакованные данные, а нужен, чтобы выравнивать данные. «x» при запаковке забиваются пустыми байтами. Пример: «пусто-число-пусто-пусто-число-пусто»:

>>> struct.pack(">xBxxBx", 255, 128)
b'\x00\xff\x00\x00\x80\x00'

>>> struct.unpack('>xBxxBx', b'\x00\xff\x00\x00\x80\x00')
(255, 128)

О форматной строке

Если в форматной строке перед символом кода – число, то значит этот символ повторяется столько раз, сколько указывает число. Два кусочка кода аналогичны:

>>> struct.pack(">3h", 1, 2, 3)
b'\x00\x01\x00\x02\x00\x03'

>>> struct.pack(">hhh", 1, 2, 3)
b'\x00\x01\x00\x02\x00\x03'

Для строк (коды «s» и «p«) надо указывать число байт – длину строки, иначе будет считаться 1 байт:

>>> struct.pack("ss", b"abc", b"XYZW")  # не указал длину - потерял байты
b'aX'

>>> struct.pack("3s4s", b"abc", b"XYZW")
b'abcXYZW'

10s – одна 10-символьная строка, а 10c – 10 отдельных символов:

>>> struct.unpack('10c', b'abracadabr')
(b'a', b'b', b'r', b'a', b'c', b'a', b'd', b'a', b'b', b'r')

>>> struct.unpack('10s', b'abracadabr')
(b'abracadabr',)

Можно вставлять пробелы между отдельными элементами форматной строки (но нельзя отделать число от символа). Пробелы игнорируются при чтении строки и нужны для удобства чтения кода программистом:

>>> struct.pack('>6sh?', b'python', 65, True)
b'python\x00A\x01'

>>> struct.pack('> 6s h ?', b'python', 65, True)  # тоже, но с пробелами
b'python\x00A\x01'

>>> struct.unpack('> 6s h ?', b'python\x00A\x01')
(b'python', 65, True)

Полезности

Можно вычислить размер данных из форматной строки без фактической запаковки или распаковки данных:

>>> struct.calcsize('> 6s h ?')
9

Удобно распаковывать байты прямо в именованные кортежи:

>>> from collections import namedtuple
>>> Student = namedtuple('Student', 'name serialnum school gradelevel')
>>> record = b'raymond   \x32\x12\x08\x01\x08'
>>> Student._make(struct.unpack('<10sHHb', record))
Student(name=b'raymond   ', serialnum=4658, school=264, gradelevel=8)

Запаковка в буффер со смещением struct.pack_into(formatbufferoffsetv1v2...):

>>> buffer = bytearray(40)
>>> struct.pack_into('h l', buffer, 10, 3432, 340840)
>>> buffer
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\r\x00\x00\x00\x00\x00\x00h3\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
>>>

Распаковка из буффера со смещением:

>>> x, y = struct.unpack_from('h l', buffer, 10)
>>> x, y
(3432, 340840)

Распаковка нескольких однотипных структур:

>>> chunks = struct.pack('hh', 10, 20) * 5  
>>> chunks  # 5 одинаковых штук
b'\n\x00\x14\x00\n\x00\x14\x00\n\x00\x14\x00\n\x00\x14\x00\n\x00\x14\x00'

>>> [(x, y) for x, y in struct.iter_unpack('hh', chunks)]
[(10, 20), (10, 20), (10, 20), (10, 20), (10, 20)]

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈