python 我如何解压缩一个长度前缀的字节串?

ni65a41a  于 2023-01-29  发布在  Python
关注(0)|答案(6)|浏览(178)

我使用标准库struct模块将bytes对象打包成一个字符串,前面加上长度信息:

>>> import struct
>>> string = b'-'
>>> t = struct.pack(">h%ds" % len(string), len(string), string)
>>> print(t)
b'\x00\x01-'

当然,我可以只删除长度计数来恢复原始数据,但是我如何在考虑长度计数的情况下 * 解包 * 这个数据,以便恢复b'-'呢?

xuo3flqw

xuo3flqw1#

通常你不会使用struct.pack把长度头和值放在一起,而是直接使用struct.pack(">h", len(data)),通过线路发送(例如在网络协议中),然后发送数据,不需要创建新的字节缓冲区。
在您的情况下,您可以简单地执行以下操作:

dataLength, = struct.unpack(">h", t[:2])
data = t[2:2+dataLength]

但正如我所说,如果您有一个基于套接字的应用程序,它将是这样的:

header = receive(2)
dataLength, = struct.unpack(">h", header)
data = receive(dataLength)
44u64gxh

44u64gxh2#

import struct
string = b'-'
fmt=">h%ds" % len(string)

下面是长度和字符串的打包:

t = struct.pack(fmt, len(string), string)
print(repr(t))
# '\x00\x01-'

因此,当您解包时,应该期望返回两个值,即长度和字符串:

length,string2=struct.unpack(fmt,t)
print(repr(string2))
# '-'

一般来说,如果你不知道字符串是如何打包的,那么就没有万无一失的方法来恢复数据,你只能猜测了!
如果你知道数据是由字符串的长度和字符串本身组成的,那么你可以尝试试错法:

import struct
string = b'-'
fmt=">h%ds" % len(string)
t = struct.pack(fmt, len(string), string)
print(repr(t))

for endian in ('>','<'):
    for fmt,size in (('b',1),('B',1),('h',2),('H',2),('i',4),('I',4),
                     ('l',4),('L',4),('q',8),('Q',8)):
        fmt=endian+fmt
        try:
            length,=struct.unpack(fmt,t[:size])
        except struct.error:
            pass
        else:
            fmt=fmt+'{0}s'.format(length)
            try:
                length,string2=struct.unpack(fmt,t)
            except struct.error:
                pass
            else:
                print(fmt,length,string2)
# ('>h1s', 1, '-')
# ('>H1s', 1, '-')

有可能组成一个不明确的字符串t,它有多个有效的解包,这将导致不同的string2,但是,我不确定。

3bygqnnd

3bygqnnd3#

struct模块是为固定格式的数据块设计的。但是,您可以使用以下代码:

import struct
t=b'\x00\x01-'
(length,)=struct.unpack_from(">h", t)
(text,)=struct.unpack_from("%ds"%length, t, struct.calcsize(">h"))
print text
dba5bblo

dba5bblo4#

假设data是一大块字节,并且您已经成功解析出了前posn个字节,该字节块的文档说明下一项是一个字节串,前面是一个16位有符号整数(不太可能,但您确实说过h)。
下面是该怎么做:

nbytes, = struct.unpack('>h', data[posn:posn+2])
posn += 2
the_string = data[posn:posn+nbytes]
posn += nbytes

现在你已经准备好下一个项目了。
在Python 2.5及更高版本中,可以使用unpack_from()代替切片。

goqiplq2

goqiplq25#

通常,在这样的"二进制格式"中,给一些数据加上前缀长度的目的是为了让解包代码知道有多少数据。
但是,使用struct.unpack不可能一次性解包所有内容--因为struct模块使用的格式是"提前计算"的。这在打包端很好,因为所有数据都可用。但在解包端不起作用,因为信息需要动态发现。
换句话说:当我们解压缩像b'\x00\x01-'这样的数据时,* 知道它是用OP示例代码中的方法压缩的 *,我们不能预先创建一个对数据正确的格式字符串。要创建这个字符串,我们需要长度,但长度是 * in data *。

struct.unpack_from

因此,不可避免地,我们将需要进行两次单独的尝试来读取数据。由于我们只想考虑部分数据,因此我们使用struct.unpack_from而不是struct.unpack。最简单的方法如下:
1.从头开始解压缩前两个字节,以找出长度。
1.使用该长度,解压缩任意多个字节,也就是说,从刚好在长度计数之后开始。
如文档中所述:
一个六个一x一个七个一x一个八个一x * 一个九个一x * 一个十个一x
根据格式字符串 * format *,从位置 * offset * 开始的 * buffer * 解包。结果是一个元组,即使它只包含一个项。从位置 * offset * 开始的缓冲区大小(以字节为单位)必须至少为格式所需的大小,如calcsize()所反映。
因此:

>>> length_format = '>h'
>>> length, = struct.unpack_from('>h', t)
>>> data, = struct.unpack_from(f'{length}s', t, 2)
>>> data
b'-'

注意结尾逗号:这些用于解包元组(不是在结构解包意义上,而是由struct.unpack_from返回的native Python sense)。
当然,第二次调用中的2说明了第一次解压缩的数据量,对于更一般的情况,或者如果这看起来太神奇,可以通过对格式字符串调用struct.calcsize来计算解压缩的数据量。

历史记录

这个问题很久以前就提出了,当时可能还没有现代工具。
在Python 3.6之前,需要对字符串调用.format,而不是使用f字符串,来创建第二个格式字符串;因此,'{}s'.format(length)。在Python 2.6之前,必须使用与OP:'%ds' % length.
在2.5之前,struct.unpack_from不可用。要解决此问题,请显式地对字符串进行适当的切片,然后使用unpack

>>> length_format = '>h'
>>> size = struct.calcsize(length_format)
>>> length, = struct.unpack(length_format, t[:size])
>>> length_size = struct.calcsize(length_format)
>>> length, = struct.unpack(length_format, t[:length_size])
>>> data, = struct.unpack(f'%ds' % length, t[length_size:])
>>> data
b'-'

其他注意事项:流式传输数据以及一次处理一种类型的数据

当然,struct中的所有功能都需要在缓冲区中工作。(例如网络连接或以二进制模式打开的文件),则必须完全读取它才能使用struct.unpackstruct.unpack_from。这可能会浪费大量内存,并且考虑到我们无论如何都需要在两个单独的步骤中考虑数据,这是没有意义的。
让我们对输入流建模:

>>> import io
>>> stream = io.BytesIO(t)

由于每次读取都是并发的,我们不需要跟踪偏移量,而是每次读取适当数量的字节。使用struct模块:

>>> length_format = '>h'
>>> length_size = struct.calcsize(length_format)
>>> length, = struct.unpack(length_format, stream.read(length_size))
>>> data, = struct.unpack(f'{length}s', stream.read(length))
>>> data
b'-'

但是现在很明显,struct模块对于解释数据的任务来说是多余的,第一次读取的只是代表整数的几个字节;int类型已经知道如何解释它。对于第二次读取,stream.read(length)已经是所需的数据,因此没有理由进行任何更多的处理。

>>> length = int.from_bytes(stream.read(length_size), 'big')
>>> data = stream.read(length)
>>> data
b'-'
fquxozlt

fquxozlt6#

你到底要怎么拆包?

>>> string = b'-'
>>> format = '>h%ds' % len(string)
>>> format
'>h1s'
>>> struct.calcsize(format)
3

对于unpack(fmt, string)len(string)必须等于struct.calcsize(fmt),因此解压缩的数据不可能只是'-'
但是:

>>> t = b'\x00\x01-'
>>> length, data = struct.unpack(format, t)
>>> length, data
(1, '-')

现在您可以使用data

相关问题