J'essaie de maîtriser le compactage et le décompactage des données binaires dans Python 3. Ce n'est en fait pas si difficile à comprendre, sauf un problème:
que se passe-t-il si j'ai une chaîne de texte de longueur variable et que je veux emballer et décompresser celle-ci de la manière la plus élégante?
D'après ce que je peux dire dans le manuel, je ne peux décompresser que les chaînes de taille fixe directement? Dans ce cas, y a-t-il un moyen élégant de contourner cette limitation sans ajouter beaucoup de zéros inutiles?
Le module struct
ne prend en charge que les structures de longueur fixe. Pour les chaînes de longueur variable, vos options sont les suivantes:
Construisez dynamiquement votre chaîne de format (une str
devra être convertie en une bytes
avant de la passer à pack()
):
s = bytes(s, 'utf-8') # Or other appropriate encoding
struct.pack("I%ds" % (len(s),), len(s), s)
Ignorez struct
et utilisez simplement les méthodes de chaîne habituelles pour ajouter la chaîne à votre sortie pack()
- ed: struct.pack("I", len(s)) + s
Pour décompresser, il suffit de décompresser un à un:
(i,), data = struct.unpack("I", data[:4]), data[4:]
s, data = data[:i], data[i:]
Si vous faites beaucoup de cela, vous pouvez toujours ajouter une fonction d'assistance qui utilise calcsize
pour effectuer le découpage en chaîne:
def unpack_helper(fmt, data):
size = struct.calcsize(fmt)
return struct.unpack(fmt, data[:size]), data[size:]
J'ai googlé cette question et quelques solutions.
Une solution élaborée et flexible.
Au lieu d'écrire du code impératif pour analyser une donnée, vous définissez de manière déclarative une structure de données décrivant vos données. Comme cette structure de données n'est pas du code, vous pouvez l'utiliser dans un sens pour analyser des données dans des objets Pythonic, et dans l'autre sens, convertir des objets («construits») en données binaires.
La bibliothèque fournit à la fois des constructions atomiques simples (telles que des entiers de différentes tailles) et des constructions composites vous permettant de former des structures hiérarchiques de complexité croissante. Construisez des caractéristiques de granularité en bits et en octets, un débogage et des tests faciles, un système de sous-classe facile à étendre et de nombreuses constructions primitives pour faciliter votre travail:
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"
Une solution rapide si vous n’avez besoin que d’une extension struct
pour les séquences d’octets de longueur variable. L'imbrication d'une structure de longueur variable peut être réalisée en pack
ing les premiers résultats pack
.
NetStruct prend en charge un nouveau caractère de mise en forme, le signe dollar ($). Le signe dollar représente une chaîne de longueur variable, codée avec sa longueur précédant la chaîne elle-même.
edit: on dirait que la longueur d'une chaîne de longueur variable utilise le même type de données que les éléments. Ainsi, la longueur maximale d'une chaîne d'octets de longueur variable est de 255, si mots - 65535, etc.
import netstruct
>>> netstruct.pack(b"b$", b"Hello World!")
b'\x0cHello World!'
>>> netstruct.unpack(b"b$", b"\x0cHello World!")
[b'Hello World!']
Voici quelques fonctions de wrapper que j'ai écrites qui aident, elles semblent fonctionner.
Voici l'aide de déballage:
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
Voici l'assistant d'emballage:
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
Pour emballer utiliser
packed=bytes('sample string','utf-8')
Pour décompresser utiliser
string=str(packed)[2:][:-1]
Cela ne fonctionne que sur la chaîne utf-8 et une solution de contournement assez simple.
Une manière simple de pouvoir faire une longueur variable lorsque vous composez une chaîne est la suivante:
pack('{}s'.format(len(string)), string)
lors du déballage, c'est un peu de la même manière
unpack('{}s'.format(len(data)), data)
Bien, mais ne peut pas gérer le nombre de champs numériques, tels que '6B' pour 'BBBBBB' La solution consisterait à développer la chaîne de format dans les deux fonctions avant utilisation. Je suis venu avec ceci:
def pack(fmt, *args):
fmt = re.sub('(\d+)([^\ds])', lambda x: x.group(2) * int(x.group(1)), fmt)
...
Et pareil pour décompresser. Peut-être pas le plus élégant, mais ça marche :)