python使用struct模塊實現(xiàn)打包/解包二進制數(shù)據(jù)
Python 有一個內(nèi)置模塊 struct,從名字上看這和 C 的結(jié)構(gòu)體有著千絲萬縷的聯(lián)系,C 的結(jié)構(gòu)體是由多個數(shù)據(jù)組合而成的一種新的數(shù)據(jù)類型。
typedef?struct?{ ????char?*name; ????int?age; ????char?*?gender; ????long?salary; }?People;
struct 模塊也是負責將多個不同類型的數(shù)據(jù)組合在一起,因為網(wǎng)絡傳輸?shù)臄?shù)據(jù)都是二進制字節(jié)流。而 Python 只有字符串可以直接轉(zhuǎn)成字節(jié)流,對于整數(shù)、浮點數(shù)則無能為力了。所以 Python 提供了 struct 模塊來幫我們解決這一點,下面來看看它的用法。
打包和解包
struct 模塊內(nèi)部有兩個函數(shù)用于打包和解包,分別是 pack 和 unpack。
- pack:將數(shù)據(jù)打包成二進制字節(jié)流;
- unpack:對二進制字節(jié)流進行解包;
import?binascii import?struct # values 包含一個 12 字節(jié)的字節(jié)串、一個整數(shù)、以及一個浮點數(shù)。 values?=?("古明地覺".encode("utf-8"),?17,?156.7) #?第一個參數(shù)?"12s?i?f"?表示格式化字符串(format),里面的符號則代表數(shù)據(jù)的類型 # 12s:12 個字節(jié)的字節(jié)串、i:?整數(shù)、f:?浮點數(shù) #?因此 12s i f 表示打包的數(shù)據(jù)有三個,分別是:12 個字節(jié)的字節(jié)串、一個整數(shù)、以及一個浮點數(shù) #?中間使用的空格只是用來對表示類型的符號進行分隔,在編譯時會被忽略 packed_data?=?struct.pack("12s?i?f",?*values)??#?這里需要使用?*?將元組打開 #?查看打印的結(jié)果 print(packed_data) """ b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89\x11\x00\x00\x003\xb3\x1cC' """ #?還可以將打包后的結(jié)果轉(zhuǎn)成十六進制,?這樣傳輸起來更加方便 print(binascii.hexlify(packed_data)) """ b'e58fa4e6988ee59cb0e8a7891100000033b31c43' """
代碼中的 packed_data 就是打包之后的結(jié)果,而我們又將其轉(zhuǎn)成了 16 進制表示。那么問題來了,既然能打包,那么肯定也能解包。
import?struct import?binascii #?之前對打包之后的數(shù)據(jù)轉(zhuǎn)成?16?進制所得到的結(jié)果 data?=?b'e58fa4e6988ee59cb0e8a7891100000033b31c43' #?所以可以使用?binascii.unhexlify?將其轉(zhuǎn)回來,得到?struct?打包之后的數(shù)據(jù) packed_data?=?binascii.unhexlify(data) #?然后調(diào)用?struct.unpack?進行解包,打包用的什么格式,解包也用什么格式 #?會得到一個元組,哪怕解包之后只有一個元素,得到的也是元組 values?=?struct.unpack("12s?i?f",?packed_data) print(str(values[0],?encoding="utf-8"))??#?古明地覺 print(values[1])??#?17 print(values[2])??#?156.6999969482422
發(fā)送端將數(shù)據(jù)按照某種格式轉(zhuǎn)成二進制字節(jié)流,接收端在接收到數(shù)據(jù)之后再按照相同的格式轉(zhuǎn)成相應的數(shù)據(jù)就行。只不過 Python 中,只有字符串可以直接轉(zhuǎn)換成二進制字節(jié)流,整數(shù)、浮點數(shù)則需要借助于 struct 模塊。
但是注意:在使用 struct 打包的時候,不能直接對字符串打包,而是需要先將字符串編碼成bytes對象。因為中文字符采用不同的編碼所占的字節(jié)數(shù)不同,所以需要先手動編碼成 bytes 對象。
整體還是比較簡單的,就是將數(shù)據(jù)按照指定格式進行打包,然后再按照指定格式進行解包。而像 12s、i、f 這些都屬于我們定義的格式中的類型指示符,而除了這些指示符之外,還有哪些類型指示符呢?
然后需要注意,我們在進行打包的時候,類型以及個數(shù)一定要匹配。
import?struct try: ????struct.pack("iii",?1,?2,?3.14) except?Exception?as?e: ????print(e)??#?required?argument?is?not?an?integer #?告訴我們需要的是整數(shù),?但我們傳遞了浮點數(shù) try: ????#?iii?表示接收?3?個整數(shù),?但我們只傳遞了兩個 ????struct.pack("iii",?1,?2) except?Exception?as?e: ????print(e)??#?pack?expected?3?items?for?packing?(got?2) try: ????#?iii?表示接收?3?個整數(shù),?但我們卻傳遞了四個 ????struct.pack("iii",?1,?2,?3,?4) except?Exception?as?e: ????print(e)??#?pack?expected?3?items?for?packing?(got?4)
此外,我們之前說一個長度為 12 的字節(jié)串,可以使用 12s 來表示,那么 3s 就表示長度為 3 的字節(jié)串。問題來了,i 表示整數(shù),那么3i 表示什么呢?
import?struct try: ????struct.pack("3i",?1,?2) except?Exception?as?e: ????print(e)??#?pack?expected?3?items?for?packing?(got?2) #?告訴我們接收?3?個值,?但是只傳遞了兩個 packed_data?=?struct.pack("3i",?1,?2,?3) print(struct.unpack("3i",?packed_data))??#?(1,?2,?3)
我們看到 3i 在結(jié)果上等同于 iii,但對于 s 而言,3s 可不等同于 sss。3s 仍然表示接收一個元素,只不過這個元素是字節(jié)串,并且長度為 3。這些細節(jié)要注意。
當然對于字符串而言,即使長度不一樣也是無所謂的,我們舉個例子。
import?struct #?第一個值是整數(shù),?第二個值是字節(jié)串(長度應該為3,?但不是3也可以) packed_data?=?struct.pack("i3s",?6,?b"abcdefg") print(packed_data)??#?b'\x06\x00\x00\x00abc' #?我們看到被截斷了,?只剩下了?abc packed_data?=?struct.pack("i6s",?6,?b"abc") print(packed_data)??#?b'\x06\x00\x00\x00abc\x00\x00\x00' #?6s?需要字節(jié)長度為?6,?但是我們只有三個,?所以在結(jié)尾補上了?3?個?\0
總之在使用 struct 進行打包的時候,需要記住兩點:
- 元素個數(shù)和符號個數(shù)要對應, 比如 3i3s3f 表示接收 7 個元素,依次是 3 個整數(shù)、一個字節(jié)串、3 個浮點數(shù);
- 元素類型和符號要對應,比如 i 對應整數(shù),s 對應字節(jié)串等等;并且對于 s 而言,前面的數(shù)字表示接收的字節(jié)串的長度;
而對于解包而言,我們也需要關注,但只需要關注一點,那就是大小。怎么理解呢?來舉個例子:
import?struct packed_data?=?struct.pack("ii",?1,?2) print(packed_data)??#?b'\x01\x00\x00\x00\x02\x00\x00\x00' #?因為 i 表示 C 的 int, 而 C 的一個 int 占 4 字節(jié), 所以結(jié)果是 8 字節(jié)。 #?只不過?1?和?2?只需一字節(jié)即可存儲,?因此其它的部分都是?0 #?打包之后的?packed_data?的大小,?不取決于打包的元素,?而是取決于格式化字符串中的類型符號 #?比如?struct.pack("4s",?b"abc")?,?盡管傳遞的字節(jié)串只有?3?字節(jié) #?但指定的是?4s,?所以打包之后的?packed_data?占?4?字節(jié) #?而我們在解包的時候,?指定的符號的字節(jié)大小?和?packed_data?要匹配 #?比如這里的?packed_data?是?8?字節(jié),?在打包結(jié)束之后它的大小就已經(jīng)固定了 try: ????print(struct.unpack("i",?packed_data)) except?Exception?as?e: ????print(e)??#?unpack?requires?a?buffer?of?4?bytes #?告訴我們需要一個?4?字節(jié)的buffer,?這是因為我們的?packed_data?是?8?字節(jié) #?同理: try: ????print(struct.unpack("iii",?packed_data)) except?Exception?as?e: ????print(e)??#?unpack?requires?a?buffer?of?12?bytes #?這樣也是不可以的,?告訴我們需要?12?字節(jié) #?只有字節(jié)數(shù)匹配,?才可以正常解析 print(struct.unpack("ii",?packed_data))??#?(1,?2)
那么問題來了,我們說一個 long long 是占 8 字節(jié),正好對應兩個 int,那么將兩個 int 按照一個 long long 來解析可不可以呢?再有 8s 也是 8 字節(jié),又可不可以進行解析呢?我們來試一下。
import?struct packed_data?=?struct.pack("ii",?1,?2) print(packed_data) """ b'\x01\x00\x00\x00\x02\x00\x00\x00' """ print(struct.unpack("8s",?packed_data)) """ (b'\x01\x00\x00\x00\x02\x00\x00\x00',) """ print(struct.unpack("q",?packed_data)) """ (8589934593,) """ #?答案顯然是可以的,?因為字節(jié)數(shù)是相匹配的 #?對于?8s?而言,?我們看到解析出來的結(jié)果就是原始的字節(jié)流 #?對于?q,?也可以正確解析,?只不過結(jié)果不是我們想要的 #?但是我們觀察一下按照?q?解析出來的結(jié)果,?結(jié)果是?8589934593,?那么它是怎么得到的呢? #?如果將?packet_data?按照?8?字節(jié)整數(shù)解析,相當于將兩個?4?字節(jié)整數(shù)合并成一個?8?字節(jié)整數(shù) #?其中整數(shù)?2?占據(jù)前?32?個位,整數(shù)?1?占據(jù)后?32?個位 print((2?<<?32)?+?1) """ 8589934593 """ #?怎么樣,?結(jié)果是不是一樣呢??至于這里為什么不是?(1?<<?31)?+?2,?我們后面會說
所以在解析的時候,格式化字符串中的類型符號對應的字節(jié)數(shù),要和 packed_data 的字節(jié)數(shù)相匹配,這是不報錯的前提。當然如果想得到正確的結(jié)果,最關鍵的還是解包對應的格式化字符串,要和打包對應的格式化字符串保持一致。
struct.Struct
在 struct 模塊中,我們可以直接使用 struct.pack 和 struct.unpack 這兩個模塊級的函數(shù),但是 struct 模塊還提供了一個 Struct 類。
import?struct s?=?struct.Struct("ii") #?和使用?struct.pack("ii",?1,?2)?之間是等價的 packed_data?=?s.pack(1,?2)? print(packed_data)?? """ b'\x01\x00\x00\x00\x02\x00\x00\x00' """
如果我們需要使用同一種格式化字符串來對大量數(shù)據(jù)進行打包的話,那么使用 struct.Struct 是更推薦的,可以類比正則。
re.search(pattern, string) 這個過程分為兩步,會先將 pattern 進行編譯轉(zhuǎn)換,然后再進行匹配。如果我們需要同一個 pattern 匹配 100 個字符串的話,那么要編譯轉(zhuǎn)換 100 次。
而如果先對 pattern 進行編譯 comp = re.compile(pattern),那么不管調(diào)用 comp.search(string) 多少次,都只會進行一次編譯轉(zhuǎn)換,效率會更高。struct 也是類似的,如果要按照相同的格式進行多次打包,那么創(chuàng)建一個 Struct 實例并在這個實例上調(diào)用方法時(不使用模塊級函數(shù))會更高效。
當然,使用 Struct 類還有一個好處,就是可以獲取一些額外信息。
import?struct s?=?struct.Struct("ii4sf") print("格式化字符串:",?s.format)??#?格式化字符串:?ii4sf print("字節(jié)數(shù):",?s.size)??#?字節(jié)數(shù):?16
我們看到打包后的數(shù)據(jù)大小是由格式化字符串中的符號所決定的。
字節(jié)序
說到字節(jié)序,你應該會想到大端存儲、小端存儲,所謂大端存儲就是:數(shù)據(jù)的低位存儲在內(nèi)存的高地址中,高位存儲在內(nèi)存的低地址中。而小端存儲與之相反:數(shù)據(jù)的低位存儲在內(nèi)存的低地址中,高位存儲在內(nèi)存的高地址中。
那么 Python 的 struct 默認使用什么存儲呢?答案是小端存儲。
import?struct #?i?表示?int32,那么相應的整數(shù)?1?就占?4?字節(jié) #?其中最低位存儲的是?1,剩余三個位存儲的是?0 packed_data?=?struct.pack("i",?1) print(packed_data)??#?b'\x01\x00\x00\x00' #?打包之后的數(shù)據(jù)是一個字節(jié)串,或者理解為?C?的字符數(shù)組 #?而數(shù)組的元素,從左往右對應的內(nèi)存地址是依次增大的 #?所以結(jié)果就是低位存在了低地址中,所以這里是小端存儲 #?而如果想要變成大端存儲的話,?可以這么做 packed_data?=?struct.pack(">i",?1) print(packed_data)??#?b'\x00\x00\x00\x01' #?我們看到此時結(jié)果就變了
當然我們在解析的時候也需要注意大小端的問題,如果是打包的時候使用的是大端存儲,那么解包的時候也要使用大端存儲。
import?struct #?因為?\x01?在最后面,而后面表示內(nèi)存的高地址 #?此時這個數(shù)字如果想表示?1,?那么它一定是大端存儲的?1 #?也就是將低位的?\x01?放在了高地址中 packed_data?=?b'\x00\x00\x00\x01' #?這里我們不指定大小端,?默認是小端 print(struct.unpack("i",?packed_data)) """ (16777216,) """ #?我們看到結(jié)果變了,?至于這個結(jié)果怎么來的,?很簡單 #?無論打包還是解包,如果不指定字節(jié)序,那么默認都是小端,低位存在低地址中 #?所以?b'\x00\x00\x00\x01'?等價于如下 print(0b00000001_00000000_00000000_00000000)? """ 16777216 """ #?所以我們也要用大端存儲進行解析,?表示:?我是大端存儲,?存儲在高地址的是數(shù)據(jù)的低位 print(struct.unpack(">i",?packed_data))? """ (1,) """ print(0b00000000_00000000_00000000_00000001)?? """ 1 """ #?所以通過?b'\x00\x00\x00\x01'?的值是不是 1,可以判斷當前采用的是大端還是小端 #?因為 \x01 在高地址,如果值為 1,說明 \x01 是低位,因此是大端,否則小端
然后再回顧一下之前的例子,我們用一個 long long 表示兩個 int。
import?struct packed_data?=?struct.pack("ii",?1,?2) print(packed_data) """ b'\x01\x00\x00\x00\x02\x00\x00\x00' """ #?將兩個?int?轉(zhuǎn)成一個?long?long,默認是小端,所以低位存在低地址中 #?因此?\x01\x00\x00\x00?表示?1,占據(jù)低?32?個位 #?\x02\x00\x00\x00?表示?2,占據(jù)高?32?個位 print(struct.unpack("q",?packed_data)) print((2?<<?32)?+?1) """ (8589934593,) 8589934593 """ #?如果按照大端解析的話,低位存在高地址中,那么就是相反的 #?但此時?\x01\x00\x00\x00?表示的不再是?1 #?同理?\x02\x00\x00\x00?表示也不再是?2 print(struct.unpack(">q",?packed_data)) print( ????(0b00000001_00000000_00000000_00000000?<<?32)?+ ????0b00000010_00000000_00000000_00000000 ) """ (72057594071482368,) 72057594071482368 """
因此 struct 模塊給我們提供了自定義字節(jié)序的功能,可以顯式地指定是使用大端存儲、還是小端存儲。而方法也很簡單,只需要給格式化字符串的第一個字符指定為特定的符號即可實現(xiàn)這一點。
>
: 大端字節(jié)序(Big-endian)<
: 小端字節(jié)序(Little-endian)!
: 網(wǎng)絡字節(jié)序(實際上就是大端字節(jié)序)=
: 本地字節(jié)序(當前系統(tǒng)的字節(jié)序)@
: 本地字節(jié)序(和=
相同,但可能有不同的對齊方式)
緩沖區(qū)
pack 方法在打包的時候,會為打包數(shù)據(jù)申請一塊內(nèi)存空間,也就是說每一次 pack 都需要申請內(nèi)存資源,顯然這是一種浪費。通過避免為每個打包數(shù)據(jù)分配一個新緩沖區(qū),在內(nèi)存開銷上可以得到優(yōu)化。而 pack_into 和 pack_from 可以支持我們從指定的緩沖區(qū)進行讀取和寫入。
import?struct import?ctypes #?創(chuàng)建一個?string?緩存,?大小為?10 buf?=?ctypes.create_string_buffer(10) #?raw?表示原始數(shù)據(jù),這里都是?\0,因為?C?中是通過?\0?來標識一個字符串的結(jié)束 print(buf.raw)??#?b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' #?value?就是?Python?中的字符串,?顯然為空 print(buf.value)??#?b'' #?然后我們進行打包,?第二個參數(shù)表示緩沖區(qū) #?第三個參數(shù)表示偏移量,?0表示從頭開始寫入;?然后后面的參數(shù)就是打包的數(shù)據(jù) struct.pack_into("ii2s",?buf,?0,?123,?345,?b"ab") #?打包之后的數(shù)據(jù)會存在?buf?中,解包的話,使用 unpack_from #?會從?buf?中讀取數(shù)據(jù)并解析,第三個參數(shù)表示從偏移量為 0 的位置開始解析 values?=?struct.unpack_from("ii2s",?buf,?0) print(values)??#?(123,?345,?b'ab')
這里的 pack_into 不會產(chǎn)生新的內(nèi)存空間,都是對 buf 進行操作。另外我們還看到了偏移量,所以可以將多個打包的數(shù)據(jù)寫入到同一個 buf 中,然后也可以從同一個 buf 中進行解包,而保證數(shù)據(jù)不沖突的前提正是這里的偏移量,舉個栗子:
import?struct import?ctypes s1?=?struct.Struct("ii6si") s2?=?struct.Struct("2s") buf?=?ctypes.create_string_buffer(s1.size?+?s2.size) s1.pack_into(buf,?0,?1,?2,?b"abcdef",?3) #?偏移量為?s1.size s2.pack_into(buf,?s1.size,?b"gh") #?從?si.size?開始解析 print(s2.unpack_from(buf,?s1.size))??#?(b'gh',) #?從?0?開始解析,解析?s1.size?個字節(jié) print(s1.unpack_from(buf,?0))??#?(1,?2,?b'abcdef',?3)
以上就是 struct 模塊,它定義了Python中整數(shù)、浮點數(shù)和二進制流之間的通用轉(zhuǎn)換邏輯。
到此這篇關于python使用struct模塊實現(xiàn)打包/解包二進制數(shù)據(jù)的文章就介紹到這了,更多相關python struct內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python爬蟲:將headers請求頭字符串轉(zhuǎn)為字典的方法
今天小編就為大家分享一篇Python爬蟲:將headers請求頭字符串轉(zhuǎn)為字典的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08pymysql.err.DataError:1366的報錯解決
通過python把數(shù)據(jù)同步至mysql數(shù)據(jù)庫的過程中,遇到錯誤,本文主要介紹了pymysql.err.DataError:1366的報錯解決,具有一定的參考價值,感興趣的可以了解一下2024-05-05聊聊Numpy.array中[:]和[::]的區(qū)別在哪
這篇文章主要介紹了在Numpy.array中[:]和[::]的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05如何利用Boost.Python實現(xiàn)Python C/C++混合編程詳解
這篇文章主要給大家介紹了關于如何利用Boost.Python實現(xiàn)Python C/C++混合編程的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧2018-11-11Python實現(xiàn)對比不同字體中的同一字符的顯示效果
這篇文章主要介紹了Python實現(xiàn)對比不同字體中的同一字符的顯示效果,也就是對比不同字體中某個字的顯示效果,這在做設計時非常有用,需要的朋友可以參考下2015-04-04