在Python中使用struct模块打包和解包可变长度数组/字符串
我正在尝试理解在Python 3中如何处理二进制数据的打包和解包。其实这并不难理解,除了一个问题:
如果我有一个可变长度的文本字符串,想要以最优雅的方式进行打包和解包,该怎么做呢?
根据手册的内容,我似乎只能直接解包固定大小的字符串?在这种情况下,有没有什么优雅的方法来绕过这个限制,而不需要填充很多不必要的零呢?
7 个回答
我找到了一种简单的方法,可以在打包字符串时处理可变长度的情况:
pack('{}s'.format(len(string)), string)
在解包的时候,方法也差不多。
unpack('{}s'.format(len(data)), data)
我在网上查了这个问题,找到了几个解决方案。
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!']
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:]