упаковка и распаковка массива / строки переменной длины с использованием модуля struct в python - PullRequest
29 голосов
/ 20 сентября 2010

Я пытаюсь справиться с упаковкой и распаковкой двоичных данных в Python 3. На самом деле это не так сложно понять, за исключением одной проблемы:

что делать, если у меня есть текстовая строка переменной длины и требуетсяупаковать и распаковать это самым элегантным способом?

Насколько я могу судить из руководства, я могу распаковать только строки фиксированного размера напрямую?В таком случае, есть ли какой-нибудь элегантный способ обойти это ограничение, не дополняя множество и множество ненужных нулей?

Ответы [ 6 ]

26 голосов
/ 20 сентября 2010

Модуль struct поддерживает только структуры фиксированной длины. Для строк переменной длины вы можете выбрать:

  • Динамическое построение строки формата (str необходимо будет преобразовать в bytes перед передачей в pack()):

    s = bytes(s, 'utf-8')    # Or other appropriate encoding
    struct.pack("I%ds" % (len(s),), len(s), s)
    
  • Пропустите struct и просто используйте обычные строковые методы, чтобы добавить строку в * pack() -данный вывод: struct.pack("I", len(s)) + s

Для распаковки вам просто нужно распаковать немного за раз:

(i,), data = struct.unpack("I", data[:4]), data[4:]
s, data = data[:i], data[i:]

Если вы делаете это много, вы всегда можете добавить вспомогательную функцию, которая использует calcsize для разрезания строк:

def unpack_helper(fmt, data):
    size = struct.calcsize(fmt)
    return struct.unpack(fmt, data[:size]), data[size:]
4 голосов
/ 04 августа 2016

Я погуглил этот вопрос и пару решений.

построить

Продуманное, гибкое решение.

Вместо того чтобы писать императивный код для анализа фрагмента данных, вы декларативно определяете структуру данных, которая описывает ваши данные. Поскольку эта структура данных не является кодом, вы можете использовать ее в одном направлении для анализа данных в объекты Pythonic, а в другом направлении преобразовывать («строить») объекты в двоичные данные.

Библиотека предоставляет как простые атомарные конструкции (такие как целые числа различных размеров), так и составные, которые позволяют формировать иерархические структуры возрастающей сложности. Конструктивные особенности обеспечивают битовую и байтовую гранулярность, простую отладку и тестирование, простую в расширении систему подклассов и множество примитивных конструкций, которые облегчат вашу работу:

from construct import *

PascalString = Struct("PascalString",
    UBInt8("length"),
    Bytes("data", lambda ctx: ctx.length),
)

>>> PascalString.parse("\x05helloXXX")
Container({'length': 5, 'data': 'hello'})
>>> PascalString.build(Container(length = 6, data = "foobar"))
'\x06foobar'


PascalString2 = ExprAdapter(PascalString,
    encoder = lambda obj, ctx: Container(length = len(obj), data = obj),
    decoder = lambda obj, ctx: obj.data
)

>>> PascalString2.parse("\x05hello")
'hello'
>>> PascalString2.build("i'm a long string")
"\x11i'm a long string"

netstruct

Быстрое решение, если вам нужно только расширение struct для последовательностей байтов переменной длины. Вложение структуры переменной длины может быть достигнуто с помощью pack первых результатов pack.

NetStruct поддерживает новый символ форматирования - знак доллара ($). Знак доллара представляет строку переменной длины, закодированную с длиной, предшествующей самой строке.

edit : похоже, что длина строки переменной длины использует тот же тип данных, что и элементы. Таким образом, максимальная длина строки байтов переменной длины составляет 255, слов - 65535 и т. Д.

import netstruct
>>> netstruct.pack(b"b$", b"Hello World!")
b'\x0cHello World!'

>>> netstruct.unpack(b"b$", b"\x0cHello World!")
[b'Hello World!']
3 голосов
/ 21 апреля 2015

Вот некоторые написанные мной функции-обертки, которые помогают, похоже, они работают.

Вот вспомогательный пакет для распаковки:

def unpack_from(fmt, data, offset = 0):
    (byte_order, fmt, args) = (fmt[0], fmt[1:], ()) if fmt and fmt[0] in ('@', '=', '<', '>', '!') else ('@', fmt, ())
    fmt = filter(None, re.sub("p", "\tp\t",  fmt).split('\t'))
    for sub_fmt in fmt:
        if sub_fmt == 'p':
            (str_len,) = struct.unpack_from('B', data, offset)
            sub_fmt = str(str_len + 1) + 'p'
            sub_size = str_len + 1
        else:
            sub_fmt = byte_order + sub_fmt
            sub_size = struct.calcsize(sub_fmt)
        args += struct.unpack_from(sub_fmt, data, offset)
        offset += sub_size
    return args

Вот вспомогательный пакетный пакет:

def pack(fmt, *args):
    (byte_order, fmt, data) = (fmt[0], fmt[1:], '') if fmt and fmt[0] in ('@', '=', '<', '>', '!') else ('@', fmt, '')
    fmt = filter(None, re.sub("p", "\tp\t",  fmt).split('\t'))
    for sub_fmt in fmt:
        if sub_fmt == 'p':
            (sub_args, args) = ((args[0],), args[1:]) if len(args) > 1 else ((args[0],), [])
            sub_fmt = str(len(sub_args[0]) + 1) + 'p'
        else:
            (sub_args, args) = (args[:len(sub_fmt)], args[len(sub_fmt):])
            sub_fmt = byte_order + sub_fmt
        data += struct.pack(sub_fmt, *sub_args)
    return data
1 голос
/ 01 декабря 2017

Для упаковки используйте

packed=bytes('sample string','utf-8')

Для распаковки используйте

string=str(packed)[2:][:-1]

Это работает только для строки utf-8 и довольно простого обходного пути.

1 голос
/ 02 февраля 2017

Простой способ, которым я смог сделать переменную длину при упаковке строки:

pack('{}s'.format(len(string)), string)

при распаковке это примерно так же

unpack('{}s'.format(len(data)), data)
0 голосов
/ 22 апреля 2016

Хорошо, но не может обработать числовое количество полей, например, «6B» для «BBBBBB».Решением будет расширение строки формата в обеих функциях перед использованием.Я придумал это:

def pack(fmt, *args):
  fmt = re.sub('(\d+)([^\ds])', lambda x: x.group(2) * int(x.group(1)), fmt)
  ...

И то же самое для распаковки.Может быть, не самый элегантный, но это работает:)

...