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

50 投票
7 回答
142386 浏览
提问于 2025-04-16 04:22

我正在尝试理解在Python 3中如何处理二进制数据的打包和解包。其实这并不难理解,除了一个问题:

如果我有一个可变长度的文本字符串,想要以最优雅的方式进行打包和解包,该怎么做呢?

根据手册的内容,我似乎只能直接解包固定大小的字符串?在这种情况下,有没有什么优雅的方法来绕过这个限制,而不需要填充很多不必要的零呢?

7 个回答

7

我找到了一种简单的方法,可以在打包字符串时处理可变长度的情况:

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

在解包的时候,方法也差不多。

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

我在网上查了这个问题,找到了几个解决方案。

construct

这是一个复杂而灵活的解决方案。

与其写一堆代码去解析数据,不如用一种声明的方式来定义一个数据结构,这个结构能描述你的数据。因为这个数据结构不是代码,所以你可以用它来把数据解析成Python对象,反过来又可以把这些对象转换成二进制数据。

这个库提供了简单的基本构造(比如不同大小的整数),也有复合构造,可以让你形成越来越复杂的层次结构。Construct支持按位和按字节的细粒度操作,方便调试和测试,还有一个易于扩展的子类系统,以及很多基本构造,能让你的工作变得更简单:

更新: 适用于Python 3.x,construct版本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"

编辑: 还要注意ExprAdapter,一旦原生的PascalString不能满足你的需求,你就需要用这个了。

netstruct

如果你只需要一个用于可变长度字节序列的struct扩展,这个解决方案很快就能搞定。通过pack第一个pack的结果,可以实现嵌套的可变长度结构。

NetStruct支持一个新的格式字符,美元符号($)。这个符号表示一个可变长度的字符串,字符串前面会有它的长度信息。

编辑: 看起来可变长度字符串的长度使用与元素相同的数据类型。因此,字节的可变长度字符串的最大长度是255,如果是单词则是65535,依此类推。

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

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

struct模块只支持固定长度的结构。如果你需要处理可变长度的字符串,有两个选择:

  • 动态构建你的格式字符串(在传递给pack()之前,str类型的字符串需要转换成bytes类型):

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

撰写回答