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 を送出します。

残りのよく使うコードは、浮動小数点数とバイト文字列のためのものです。

  • f -- 32 ビット浮動小数点数(単精度。約 7 桁の十進数)。MicroPython の通常の float はすでにこのサイズなので、f にパックしても情報は失われません。

  • d -- 64 ビット浮動小数点数(倍精度。約 15 桁の十進数)。32 ビットの MicroPython の floatd にパックすると 8 バイトに拡張されますが、精度は増えません。

  • s -- 固定長のバイト文字列。前に個数を付けます(8 バイトのフィールドなら 8s)。

2.29.2. バイト順

複数バイトの整数は、メモリ内に 2 通りの方法で格納できます。32 ビットフィールド内の 0x12345678 という数値は、次のように配置されます。

  • リトルエンディアン -- 最下位バイトが先頭:78 56 34 12

  • ビッグエンディアン -- 最上位バイトが先頭:12 34 56 78

どちらも同じ値を表します。違うのは、フィールドのどちらの端が下位バイトかという点だけです。あるシステムで書き込まれたファイルは、バイト順が一致しないと、別のシステムで読み込んだときに文字化けします。

フォーマット文字列の先頭の文字でバイト順を選びます。

  • < -- リトルエンディアン。x86 や ARM で一般的です。

  • > -- ビッグエンディアン。ネットワークプロトコルで一般的です。

  • ! -- ネットワーク順。> と同等です。

先頭の文字がない場合は、ネイティブのバイト順とネイティブのアライメントが使われます。<> を明示的に指定すると、そのあいまいさがなくなります。ファイルを読んだり別のマシンと通信したりするときは、通常これが望ましい動作です。

注釈

OpenMV Cam はリトルエンディアンです -- ホスト PC と同じです。カメラローカルのファイルや、デスクトップとの間でやり取りするバイナリデータには、フォーマット文字列で < を使ってください。ネットワークプロトコルや、仕様でビッグエンディアンが求められる形式には >(または !)を使ってください。

6 バイトが 1 列に並べられており、先頭の 2 バイトが 「H」フィールド(16 ビット符号なし)として、次の 4 バイトが「I」フィールド(32 ビット符号なし)として グループ化され、それぞれリトルエンディアンのバイト順で ラベル付けされています。

"<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 より小さいかたまりを生成します -- これは、部分的なレコードをアンパックしようとするのではなく、ストリームの終端を示す条件として扱ってください。