2.29. Struct 與二進位資料¶
struct 模組會將 Python 值打包成固定的二進位佈局,並將位元組解包還原為 Python 值。當你處理二進位檔案格式、網路協定,或是交換固定大小記錄的裝置時,就可以使用它。
兩個函式涵蓋了大多數情況:
struct.pack()-- 接收 Python 值與一個 格式字串,回傳具有確切佈局的bytes物件。struct.unpack()-- 接收一個格式字串與一個bytes物件,回傳一個由 Python 值組成的元組。
2.29.1. 格式字串¶
格式字串會為記錄中的每個欄位列出一個 代碼。這些代碼同時描述了每個欄位的大小與解讀方式。
Python 的 int 沒有固定大小 -- 它會自動增長以容納你所賦予的任何值。但二進位格式 確實 有固定大小:每個整數欄位都使用約定好的位元組數。struct 會在無界限的 Python 整數與這些固定大小的表示法之間進行轉換。
整數的 寬度 是指它所使用的位元數。一個位元組為八個位元。小寫代碼是有號版本;大寫代碼是無號版本(僅限非負值):
b/B-- 8 位元(一個位元組)。有號為-128..127,無號為0..255。h/H-- 16 位元(兩個位元組)。有號為-32768..32767,無號為0..65535。i/I-- 32 位元(四個位元組)。有號約為 ±二十億,無號約為四十億。q/Q-- 64 位元(八個位元組)。對日常使用而言實際上是無界限的。
選擇一個能輕鬆涵蓋你預期範圍的寬度。打包超出宣告範圍的值時,依據建置版本不同,可能會無聲地環繞回捲,或是引發 struct.error。
其餘常見的代碼用於浮點數與位元組字串:
2.29.2. 位元組順序¶
多位元組整數可以用兩種方式儲存在記憶體中。32 位元欄位中的數字 0x12345678 會以如下方式排列:
小端序(Little-endian) -- 最低有效位元組在前:
78 56 34 12。大端序(Big-endian) -- 最高有效位元組在前:
12 34 56 78。
兩者編碼的是同一個值;它們只是在欄位的哪一端放置低位位元組這一點上有所不同。若位元組順序不相符,由某一系統寫入的檔案被另一系統讀取時就會出現亂碼。
格式字串的開頭字元決定了順序:
<-- 小端序。常見於 x86 與 ARM。>-- 大端序。常見於網路協定。!-- 網路順序,等同於>。
若沒有開頭字元,則會使用原生位元組順序與原生對齊;明確設定 < 或 > 可消除這種歧義,而這通常正是你在讀取檔案或與另一台機器通訊時所需要的。
備註
OpenMV Cam 是 小端序 -- 與其主機 PC 相同。對於相機本機的檔案,以及往返於桌面電腦的二進位資料,請在格式字串中使用 <。對於網路協定以及任何規格要求大端序的格式,請使用 >(或 !)。
"<HI" 會將一個 16 位元值後接一個 32 位元值打包成六個小端序位元組。¶
2.29.3. 打包¶
import struct
blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))
輸出::
b'@\x01@B\x0f\x00' 6
<HI 格式會產生六個位元組:兩個給 H 欄位,四個給 I 欄位,全部都是小端序。請傳入格式所預期的確切值數量 -- 數量不符會引發 struct.error。
2.29.4. 解包¶
width, count = struct.unpack("<HI", blob)
print(width, count)
輸出::
320 1000000
struct.unpack() 總是回傳一個元組,即使格式只描述單一欄位。為了可讀性,請在同一行將其解包。
2.29.5. 固定長度的位元組字串¶
s 代碼會原樣讀取或寫入一段位元組。計數放在 s 之前 -- 4s 表示「四個位元組視為單一位元組字串」。這是在記錄中嵌入魔術值、固定大小標籤或填補過的名稱欄位的常用做法:
header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)
輸出::
b'OMV0@\x01@B\x0f\x00'
前四個位元組是字面魔術值 b"OMV0";接下來兩個是 H 欄位(320);最後四個是 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 的區塊 -- 應將其視為串流結束的條件,而非試圖解包一筆不完整的記錄。