使用python中的struct模块打包和解包可变长度数组/字符串

Posted

技术标签:

【中文标题】使用python中的struct模块打包和解包可变长度数组/字符串【英文标题】:packing and unpacking variable length array/string using the struct module in python 【发布时间】:2011-04-14 19:01:52 【问题描述】:

我正在尝试掌握 Python 3 中二进制数据的打包和解包。它实际上并不难理解,除了一个问题:

如果我有一个可变长度的文本字符串并想以最优雅的方式打包和解包呢?

据我所知,我只能直接解压缩固定大小的字符串吗?在这种情况下,有没有什么优雅的方法可以绕过这个限制而不用填充大量不必要的零?

【问题讨论】:

【参考方案1】:

struct 模块只支持固定长度的结构。对于可变长度字符串,您的选择是:

动态构造您的格式字符串(str 在传递给pack() 之前必须转换为bytes):

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:]

【讨论】:

如果将长度/字符数添加到二进制数据中,您将如何解压? OP的问题特别提到了Python 3,这个答案在Python 3中不起作用,因为字符串对象不再支持缓冲区接口。 @jonesy:唯一不起作用的部分是第一个 sn-p,将str 传递给pack();现在已经解决了这个问题。 对于在二进制数据块中解压缩 C 风格的字符串也可以像这样 s.rstrip(b'\x00').decode("utf_8").【参考方案2】:

我已经用谷歌搜索了这个问题和几个解决方案。

construct

精心设计、灵活的解决方案。

您无需编写命令式代码来解析一段数据,而是以声明方式定义描述数据的数据结构。由于此数据结构不是代码,您可以在一个方向使用它来将数据解析为 Pythonic 对象,而在另一个方向将对象转换(“构建”)为二进制数据。

该库提供了简单的原子结构(例如各种大小的整数),以及允许您形成复杂度不断增加的层次结构的复合结构。 Construct 具有位和字节粒度、易于调试和测试、易于扩展的子类系统以及许多使您的工作更轻松的原始构造:

更新:Python 3.x,构造 2.10.67;他们也有原生的 PascalString,所以重命名了


    from construct import *
    
    myPascalString = Struct(
        "length" / Int8ul,
        "data" / Bytes(lambda ctx: ctx.length)
    )

    >>> myPascalString.parse(b'\x05helloXXX')
    Container(length=5, data=b'hello')
    >>> myPascalString.build(Container(length=6, data=b"foobar"))
    b'\x06foobar'


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

    >>> myPascalString2.parse(b"\x05hello")
    b'hello'

    >>> myPascalString2.build(b"i'm a long string")
    b"\x11i'm a long string"

ed:还要注意那个 ExprAdapter,一旦原生 PascalString 不能从它做你需要的事情,这就是你要做的事情。 p>

netstruct

如果您只需要为可变长度字节序列添加struct 扩展名,这是一种快速解决方案。嵌套可变长度结构可以通过packing 第一个pack 结果来实现。

NetStruct 支持新的格式化字符,美元符号 ($)。美元符号表示一个可变长度的字符串,其长度在字符串本身之前编码。

edit:看起来可变长度字符串的长度使用与元素相同的数据类型。因此,可变长度字节串的最大长度是 255,如果是 word - 65535,以此类推。

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

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

【讨论】:

您写过关于 netstruct 的文章:“看起来它只使用一个字节来表示字符串长度”。但是,$ 符号之前的格式字符表示要用于其长度的格式。您选择了b,它是一个 1 字节整数。如果您选择了hnetstruct 将使用 2 字节整数来表示长度。 很好的答案,我的学习曲线的 Alpha 和 Omega 与构造,从这个答案开始,在摆弄一天后回到 ExprAdapter 作为我的解决方案:几乎可以满足我的需要。我希望我的回答对你有帮助。【参考方案3】:

这是我编写的一些包装函数,它们似乎有用。

这是解包助手:

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

【讨论】:

【参考方案4】:

我能够在打包字符串时进行可变长度的一种简单方法是:

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

拆包的时候也是这样

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

【讨论】:

【参考方案5】:

打包使用

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

解压使用

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

这仅适用于 utf-8 字符串和非常简单的解决方法。

【讨论】:

【参考方案6】:

很好,但无法处理数字字段,例如 'BBBBBB' 的 '6B'。解决方案是在使用前在这两个函数中扩展格式字符串。我想出了这个:

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

解包也一样。也许不是最优雅的,但它有效:)

【讨论】:

【参考方案7】:

另一种愚蠢但非常简单的方法:(PS:正如其他人提到的那样,考虑到这一点,没有纯粹的打包/解包支持)

import struct


def pack_variable_length_string(s: str) -> bytes:
    str_size_bytes = struct.pack('!Q', len(s))
    str_bytes = s.encode('UTF-8')
    return str_size_bytes + str_bytes


def unpack_variable_length_string(sb: bytes, offset=0) -> (str, int):
    str_size_bytes = struct.unpack('!Q', sb[offset:offset + 8])[0]
    return sb[offset + 8:offset + 8 + str_size_bytes].decode('UTF-8'), 8 + str_size_bytes + offset


if __name__ == '__main__':
    b = pack_variable_length_string('Worked maybe?') + \
        pack_variable_length_string('It seems it did?') + \
        pack_variable_length_string('Are you sure?') + \
        pack_variable_length_string('Surely.')
    next_offset = 0
    for i in range(4):
        s, next_offset = unpack_variable_length_string(b, next_offset)
        print(s)

【讨论】:

以上是关于使用python中的struct模块打包和解包可变长度数组/字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 ctypes 打包和解包(结构 <-> str)

python中*和**的打包和解包

python中*和**的打包和解包

qt中如何解包利用python 的struct.pack()函数打包的数据

函数参数打包和解包 Python

在 Python 中打包/解包复杂数据