2.29. Struct とバイナリデータ¶
struct モジュールは、Python の値を固定されたバイナリレイアウトにパックし、バイト列を Python の値にアンパックします。バイナリファイル形式、ネットワークプロトコル、または固定サイズのレコードをやり取りするデバイスを扱うときに使います。
ほとんどのケースは 2 つの関数でカバーできます。
struct.pack()-- Python の値とフォーマット文字列を受け取り、その通りのレイアウトのbytesオブジェクトを返します。struct.unpack()-- フォーマット文字列とbytesオブジェクトを受け取り、Python の値のタプルを返します。
2.29.1. フォーマット文字列¶
フォーマット文字列は、レコード内の各フィールドごとに 1 つのコードを並べたものです。コードは各フィールドのサイズと解釈の両方を表します。
Python の int には固定サイズがありません -- 代入した値に合わせて大きくなります。一方、バイナリ形式には確かに固定サイズがあります。すべての整数フィールドは、取り決められたバイト数を使います。struct は、上限のない Python の int とこれらの固定サイズ表現との間で変換を行います。
整数の幅とは、それが使用するビット数のことです。1 バイトは 8 ビットです。小文字のコードは符号付きの変種で、大文字のコードは符号なし(非負の値のみ)です。
b/B-- 8 ビット(1 バイト)。符号付きで-128..127、符号なしで0..255。h/H-- 16 ビット(2 バイト)。符号付きで-32768..32767、符号なしで0..65535。i/I-- 32 ビット(4 バイト)。符号付きで約 ±20 億、符号なしで約 40 億。q/Q-- 64 ビット(8 バイト)。日常的な用途では事実上、上限がありません。
想定する範囲を余裕をもってカバーできる幅を選んでください。宣言した範囲外の値をパックすると、ビルドによって、暗黙のうちにラップアラウンドするか、struct.error を送出します。
残りのよく使うコードは、浮動小数点数とバイト文字列のためのものです。
2.29.2. バイト順¶
複数バイトの整数は、メモリ内に 2 通りの方法で格納できます。32 ビットフィールド内の 0x12345678 という数値は、次のように配置されます。
リトルエンディアン -- 最下位バイトが先頭:
78 56 34 12。ビッグエンディアン -- 最上位バイトが先頭:
12 34 56 78。
どちらも同じ値を表します。違うのは、フィールドのどちらの端が下位バイトかという点だけです。あるシステムで書き込まれたファイルは、バイト順が一致しないと、別のシステムで読み込んだときに文字化けします。
フォーマット文字列の先頭の文字でバイト順を選びます。
<-- リトルエンディアン。x86 や ARM で一般的です。>-- ビッグエンディアン。ネットワークプロトコルで一般的です。!-- ネットワーク順。>と同等です。
先頭の文字がない場合は、ネイティブのバイト順とネイティブのアライメントが使われます。< や > を明示的に指定すると、そのあいまいさがなくなります。ファイルを読んだり別のマシンと通信したりするときは、通常これが望ましい動作です。
注釈
OpenMV Cam はリトルエンディアンです -- ホスト PC と同じです。カメラローカルのファイルや、デスクトップとの間でやり取りするバイナリデータには、フォーマット文字列で < を使ってください。ネットワークプロトコルや、仕様でビッグエンディアンが求められる形式には >(または !)を使ってください。
"<HI" は、16 ビットの値とそれに続く 32 ビットの値を、6 バイトのリトルエンディアンにパックします。¶
2.29.3. パック¶
import struct
blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))
出力:
b'@\x01@B\x0f\x00' 6
<HI フォーマットは 6 バイトを生成します。H フィールドに 2 バイト、I フィールドに 4 バイトで、すべてリトルエンディアンです。フォーマットが期待する個数ちょうどの値を渡してください -- 数が合わないと struct.error を送出します。
2.29.4. アンパック¶
width, count = struct.unpack("<HI", blob)
print(width, count)
出力:
320 1000000
struct.unpack() は、フォーマットが単一のフィールドを表す場合でも、常にタプルを返します。読みやすさのために、同じ行でアンパックしましょう。
2.29.5. 固定長のバイト文字列¶
s コードは、バイトのかたまりをそのまま読み書きします。個数は s の前に置きます -- 4s は「単一のバイト文字列として扱う 4 バイト」を意味します。これは、レコード内にマジック値、固定サイズのタグ、またはパディングされた名前フィールドを埋め込む通常の方法です。
header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)
出力:
b'OMV0@\x01@B\x0f\x00'
最初の 4 バイトはリテラルのマジック b"OMV0"、次の 2 バイトは H フィールド(320)、最後の 4 バイトは I フィールド(1000000)です。アンパックすると、バイト列が bytes オブジェクトとして返されます。
magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)
出力:
b'OMV0' 320 1000000
元の値が宣言された個数より短い場合、結果は右側を \x00 でパディングされます。長い場合は、超過したバイトが暗黙のうちに切り捨てられます。
struct.pack("4s", b"hi") # b'hi\x00\x00'
struct.pack("4s", b"toolong") # b'tool'
個数は文字数ではなくバイト長です -- s は生のバイトを扱うため、複数バイト文字を含む UTF-8 文字列は、まず .encode() してからバイト単位で数える必要があります。
2.29.6. サイズ計算と部分読み込み¶
struct.calcsize() は、フォーマット文字列が消費するバイト数を返します。
struct.calcsize("<HI") # 6
ファイルからレコードのストリームを読むときは、レコードごとに正確にそのバイト数だけ読み込みます。
record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
while True:
chunk = f.read(record_size)
if len(chunk) < record_size:
break
width, count = struct.unpack("<HI", chunk)
print(width, count)
ファイルの末尾での読み込み不足は、record_size より小さいかたまりを生成します -- これは、部分的なレコードをアンパックしようとするのではなく、ストリームの終端を示す条件として扱ってください。