Python 3.のバイナリデータのパッキングとアンパッキングについて理解しようとしています。
可変長のテキスト文字列があり、これを最もエレガントな方法でパックおよびアンパックしたい場合はどうすればよいですか?
マニュアルからわかる限り、固定サイズの文字列のみを直接解凍できますか?その場合、たくさんの不要なゼロをパディングせずにこの制限を回避するエレガントな方法はありますか?
struct
モジュールは、固定長構造のみをサポートします。可変長文字列の場合、オプションは次のいずれかです。
書式文字列を動的に構築します(str
はpack()
に渡す前にbytes
に変換する必要があります)。
_s = bytes(s, 'utf-8') # Or other appropriate encoding
struct.pack("I%ds" % (len(s),), len(s), s)
_
struct
をスキップし、通常の文字列メソッドを使用して、文字列をpack()
- ed出力に追加します: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:]
_
この質問といくつかの解決策をグーグルで調べました。
精巧で柔軟なソリューション。
データを解析する命令型コードを記述する代わりに、データを記述するデータ構造を宣言的に定義します。このデータ構造はコードではないため、データをPythonオブジェクトに解析するために一方向で使用し、反対方向でオブジェクトをバイナリデータに変換(「ビルド」)することができます。
このライブラリは、単純でアトミックな構造(さまざまなサイズの整数など)と、複雑さが増す階層構造を形成できる複合構造の両方を提供します。ビットとバイトの粒度、簡単なデバッグとテスト、拡張しやすいサブクラスシステム、および作業を簡単にするための多くのプリミティブコンストラクトを作成します。
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"
可変長バイトシーケンスにstruct
拡張のみが必要な場合の迅速な解決策。可変長構造のネストは、最初のpack
の結果をpack
ingすることで実現できます。
NetStructは、新しいフォーマット文字であるドル記号($)をサポートしています。ドル記号は可変長文字列を表し、文字列自体の前にその長さでエンコードされます。
edit:可変長文字列の長さは要素と同じデータ型を使用しているように見えます。したがって、バイトの可変長文字列の最大長は、ワード-65535などの場合、255です。
import netstruct
>>> netstruct.pack(b"b$", b"Hello World!")
b'\x0cHello World!'
>>> netstruct.unpack(b"b$", b"\x0cHello World!")
[b'Hello World!']
ここに、私が書いたいくつかのラッパー関数を示します。これらは機能しているようです。
アンパックヘルパーは次のとおりです。
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
使用をパックするには
packed=bytes('sample string','utf-8')
開梱するには
string=str(packed)[2:][:-1]
これはutf-8文字列と非常に簡単な回避策でのみ機能します。
文字列をパックするときに可変長を実行できた簡単な方法は次のとおりです。
pack('{}s'.format(len(string)), string)
開梱するとき、それは同じようなものです
unpack('{}s'.format(len(data)), data)
いいですが、「BBBBBB」の「6B」などのフィールドの数値を処理できません。解決策は、使用する前に両方の関数でフォーマット文字列を展開することです。私はこれを思いつきました:
def pack(fmt, *args):
fmt = re.sub('(\d+)([^\ds])', lambda x: x.group(2) * int(x.group(1)), fmt)
...
開梱も同じです。たぶん最もエレガントではないかもしれませんが、それは動作します:)