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

时间:2010-09-20 16:48:24

标签: python binary struct python-3.x

我试图抓住Python 3中打包和解包二进制数据的问题。除了一个问题外,它实际上并不难理解:

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

据我从手册中可以看出,我只能直接解压缩固定大小的字符串?在这种情况下,是否有任何优雅的方法来绕过这个限制而不填充很多和不必要的零?

7 个答案:

答案 0 :(得分:26)

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

答案 1 :(得分:4)

我已经搜索了这个问题和一些解决方案。

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支持新的格式化字符,即美元符号($)。美元符号表示一个可变长度的字符串,其长度在字符串本身之前编码。

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

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

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

答案 2 :(得分: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

答案 3 :(得分:1)

在打包字符串时,我能够做一个可变长度的简单方法是:

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

解包时的方式有点相同

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

答案 4 :(得分:1)

打包使用

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

要解压缩使用

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

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

答案 5 :(得分:0)

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

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

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

答案 6 :(得分:0)

另一种愚蠢但非常简单的方法:(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)