Подтвердить что ты не робот

Упаковка и распаковка массива переменной длины/строки с использованием модуля struct в python

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

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

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

4b9b3361

Ответ 1

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

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

    s = bytes(s, 'utf-8')    # Or other appropriate encoding
    struct.pack("I%ds" % (len(s),), len(s), s)
    
  • Пропустите struct и просто используйте обычные строковые методы, чтобы добавить строку в ваш вывод pack() -ed: 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:]

Ответ 2

Вот некоторые функции оболочки, которые я написал, какую помощь они работают.

Здесь помощник распаковки:

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

Ответ 3

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

construct

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

Вместо того, чтобы писать императивный код для разбора части данных, вы декларативно определяете структуру данных, которая описывает ваши данные. Поскольку эта структура данных не является кодом, вы можете использовать ее в одном направлении для анализа данных в 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 поддерживает новый символ форматирования, знак доллара ($). Знак доллара представляет строку переменной длины, закодированную с ее длиной, предшествующей самой строке.

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

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

>>> netstruct.unpack(b"b$", b"\x0cHello World!")
[b'Hello World!']

Ответ 4

Чтобы использовать пакет

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

Чтобы распаковать использование

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

Это работает только с строкой utf-8 и довольно простым обходом.

Ответ 5

Извините, не удалось добавить комментарий к решению duncan.forster(недостаточно репутации). Приятно, но не может обрабатывать числовое число полей, например "6B" для "BBBBBB". Решением будет расширение строки формата в обеих функциях перед использованием. Я придумал это:

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

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

Ответ 6

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

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

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

unpack('{}s'.format(len(data)), data)