Python 中 struct 模塊的用法

Python 為了保持語言的簡潔,僅僅為用戶提供了幾種簡單的數據結構:int, float, str, list, dict 和 tuple。不同於編譯型語言 C/C++,在 Python 中,我們往往不需要關心不同類型的變量在解釋器內部的實現方式。例如,對於一個長整形數據,我們在 Python 2 中可以直接寫成 a=123456789012345L,而不用去考慮變量 a 佔了幾個字節。這種抽象的方式為程序的編寫提供了足夠的支持,但是在某些情況下(比如讀寫二進制文件,進行網絡 Raw Socket 編程)的時候,我們需要一些其他模塊來實現我們關於變量長度控制的需求。

struct 模塊

當我們在 Python 中跟二進制數據打交道的時候,就要用到 struct 這個模塊了。struct 模塊為 Python 與 C 的混合編程,處理二進制文件以及進行網絡協議交互提供了便利。理解這個模塊主要需要理解三個函數:

struct.pack(fmt, v1, v2, ...)

struct.unpack(fmt, string)

struct.calcsize(fmt)

第一個函數 pack 負責將不同的變量打包在一起,成為一個字節字符串,即類似於 C 語言中的字節流。第二個函數 unpack 將字節字符串解包成為變量。第三個函數 calsize 計算按照格式 fmt 打包的結果有多少個字節。這裡打包格式 fmt 確定了將變量按照什麼方式打包成字節流,其包含了一系列的格式字符串。這裡就不再給出不同格式字符串的含義了,詳細細節可以參照 Python Doc (struct).

使用 struct 打包定長結構

一般而言,在使用 struct 的時候,要打包的數據都是定長的。定長的數據代表你需要明確給出要打包的或者解包的數據長度,否則打包解包函數將會出錯。下面用例子說明什麼是定長打包:

import struct

a = struct.pack("2I3sI", 12, 34, "abc", 56)

b = struct.unpack("2I3sI", a)

print b

## 輸出 (12, 34, 'abc', 56)

上面的代碼將兩個整數 12 和 34,一個字符串 “abc” 和一個整數 56 一起打包成為一個字節字符流,然後再解包。其中打包格式中明確指出了打包的長度:"2I" 表明起始是兩個unsigned int,"3s" 表明長度為4的字符串,最後一個 "I" 表示最後緊跟一個 unsigned int。所以上面的打印 b 輸出結果是:(12, 34, ‘abc’, 56)。

我們可以調用 calcsize() 來計算 "2I3sI" 這個模式佔用的字節數:

print struct.calcsize("2I3sI")

## 輸出 16

可以看到上面的三個整型加一個 3 字符的字符串一共佔用了 16 個字節。為什麼會是 16 個字節呢?不應該是 15 個字節嗎?其實,在 struct 的打包過程中,根據特定類型的要求,必須進行字節對齊。由於默認 unsigned int 型佔用四個字節,因此要在字符串的位置進行4字節對齊,因此即使是 3 個字符的字符串也要佔用 4 個字節。

再看一下不需要字節對齊的模式:

print struct.calcsize("2Is")

## 輸出 9

由於單字符出現在兩個整型之後,不需要進行字節對齊,所以輸出結果是 9.

需要指出的是,對於 unpack 而言,只要 fmt 對應的字節數和字節字符串 string 的字節數一致,就可以成功的進行解析,否則 unpack 函數將拋出異常。例如我們也可以使用如下的 fmt 解析出 a:

c = struct.unpack("2I2sI", a)

print struct.calcsize("2I2sI")

print c

## 輸出 16 (12, 34, 'ab', 56)

可以看到這裡 unpack 解析出了字符串的前兩個字符,沒有產生任何問題。

struct 處理不定長數據

在上一節的介紹中,我們看到了在使用 pack 和 unpack 的過程中,我們需要明確的指出打包模式中每個位置的長度。比如格式 "2I3sI" 就明確指出了整型的個數和字符串的個數。有時候,我們還可能會需要處理變長的打包數據。

變長字符串的打包

例如我們在程序中可能會得到一個字符串 s,這個 s 沒有一個固定的長度,所以我們每次打包的時候都需要將 s 的長度也打包到一起,這樣我們才能進行正確的解包。其實,這種情況在處理網絡數據包中非常常見。在使用網絡編程的時候,我們可能利用報文的第一個字段記錄報文的長度。每次讀取報文的時候,我們先讀取報文的第一個字段,獲取其長度之後在處理報文內容。

我們可以採用兩種方式處理這種情況:

s = bytes(s, 'utf-8') # Or other appropriate encoding

struct.pack("I%ds" % (len(s),), len(s), s)

或者

struct.pack("I", len(s)) + s

第一種方式先將報文轉變成為字節碼,然後獲取字節碼的長度,將長度嵌入到打包之後的報文中去。可以看到格式字符串中的 "I" 就用來記錄報文的長度。第二種方式是直接將字符串的長度打包成字節字符串,再跟原始字符串做一個連接操作。

變長字符串的解包

根據上面的打包方式,我們可以輕鬆的解開打包串:

int_size = struct.calcsize("I")

(i,), data = struct.unpack("I", data[:int_size]), data[int_size:]

data_content = data[i:]

由於報文的長度 len(s) 我們使用定長的整型 "I" 進行了打包,所以解包的時候我們可以先將報文長度獲取出來,之後再根據報文長度讀取報文內容。



分享到:


相關文章: