Кто сказал, что нельзя делать низкоуровневые вещи на 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 | байт набивки | нет значения | |
c | char | bytes длины 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer | 4 |
l | long | integer | 4 |
L | unsigned long | integer | 4 |
q | long long | integer | 8 |
Q | unsigned long long | integer | 8 |
n | ssize_t | integer | зависит |
N | size_t | integer | зависит |
e | «половинный float« | float | 2 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | bytes | указывается явно |
p | char[] — строка из Паскаля | 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(format, buffer, offset, v1, v2, ...)
:
>>> 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 👈