Python 有一個(gè)內(nèi)置模塊 struct,從名字上看這和 C 的結(jié)構(gòu)體有著千絲萬(wàn)縷的聯(lián)系,C 的結(jié)構(gòu)體是由多個(gè)數(shù)據(jù)組合而成的一種新的數(shù)據(jù)類(lèi)型。 typedef struct { char *name; int age; char * gender; long salary; } People;
struct 模塊也是負(fù)責(zé)將多個(gè)不同類(lèi)型的數(shù)據(jù)組合在一起,因?yàn)榫W(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)都是二進(jìn)制字節(jié)流。而 Python 只有字符串可以直接轉(zhuǎn)成字節(jié)流,對(duì)于整數(shù)、浮點(diǎn)數(shù)則無(wú)能為力了。所以 Python 提供了 struct 模塊來(lái)幫我們解決這一點(diǎn),下面來(lái)看看它的用法。
struct 模塊內(nèi)部有兩個(gè)函數(shù)用于打包和解包,分別是 pack 和 unpack。 import binascii import struct
# values 包含一個(gè) 12 字節(jié)的字節(jié)串、一個(gè)整數(shù)、以及一個(gè)浮點(diǎn)數(shù)。 values = ("古明地覺(jué)".encode("utf-8"), 17, 156.7)
# 第一個(gè)參數(shù) "12s i f" 表示格式化字符串(format),里面的符號(hào)則代表數(shù)據(jù)的類(lèi)型 # 12s:12 個(gè)字節(jié)的字節(jié)串、i: 整數(shù)、f: 浮點(diǎn)數(shù) # 因此 12s i f 表示打包的數(shù)據(jù)有三個(gè),分別是:12 個(gè)字節(jié)的字節(jié)串、一個(gè)整數(shù)、以及一個(gè)浮點(diǎn)數(shù) # 中間使用的空格只是用來(lái)對(duì)表示類(lèi)型的符號(hào)進(jìn)行分隔,在編譯時(shí)會(huì)被忽略 packed_data = struct.pack("12s i f", *values) # 這里需要使用 * 將元組打開(kāi)
# 查看打印的結(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)成十六進(jìn)制, 這樣傳輸起來(lái)更加方便 print(binascii.hexlify(packed_data)) """ b'e58fa4e6988ee59cb0e8a7891100000033b31c43' """
代碼中的 packed_data 就是打包之后的結(jié)果,而我們又將其轉(zhuǎn)成了 16 進(jìn)制表示。那么問(wèn)題來(lái)了,既然能打包,那么肯定也能解包。 import struct import binascii
# 之前對(duì)打包之后的數(shù)據(jù)轉(zhuǎn)成 16 進(jìn)制所得到的結(jié)果 data = b'e58fa4e6988ee59cb0e8a7891100000033b31c43'
# 所以可以使用 binascii.unhexlify 將其轉(zhuǎn)回來(lái),得到 struct 打包之后的數(shù)據(jù) packed_data = binascii.unhexlify(data)
# 然后調(diào)用 struct.unpack 進(jìn)行解包,打包用的什么格式,解包也用什么格式 # 會(huì)得到一個(gè)元組,哪怕解包之后只有一個(gè)元素,得到的也是元組 values = struct.unpack("12s i f", packed_data) print(str(values[0], encoding="utf-8")) # 古明地覺(jué) print(values[1]) # 17 print(values[2]) # 156.6999969482422
發(fā)送端將數(shù)據(jù)按照某種格式轉(zhuǎn)成二進(jìn)制字節(jié)流,接收端在接收到數(shù)據(jù)之后再按照相同的格式轉(zhuǎn)成相應(yīng)的數(shù)據(jù)就行。只不過(guò) Python 中,只有字符串可以直接轉(zhuǎn)換成二進(jìn)制字節(jié)流,整數(shù)、浮點(diǎn)數(shù)則需要借助于 struct 模塊。 但是注意:在使用 struct 打包的時(shí)候,不能直接對(duì)字符串打包,而是需要先將字符串編碼成bytes對(duì)象。因?yàn)橹形淖址捎貌煌木幋a所占的字節(jié)數(shù)不同,所以需要先手動(dòng)編碼成 bytes 對(duì)象。
整體還是比較簡(jiǎn)單的,就是將數(shù)據(jù)按照指定格式進(jìn)行打包,然后再按照指定格式進(jìn)行解包。而像 12s、i、f 這些都屬于我們定義的格式中的類(lèi)型指示符,而除了這些指示符之外,還有哪些類(lèi)型指示符呢? 然后需要注意,我們?cè)谶M(jìn)行打包的時(shí)候,類(lèi)型以及個(gè)數(shù)一定要匹配。 import struct
try: struct.pack("iii", 1, 2, 3.14) except Exception as e: print(e) # required argument is not an integer # 告訴我們需要的是整數(shù), 但我們傳遞了浮點(diǎn)數(shù)
try: # iii 表示接收 3 個(gè)整數(shù), 但我們只傳遞了兩個(gè) struct.pack("iii", 1, 2) except Exception as e: print(e) # pack expected 3 items for packing (got 2)
try: # iii 表示接收 3 個(gè)整數(shù), 但我們卻傳遞了四個(gè) struct.pack("iii", 1, 2, 3, 4) except Exception as e: print(e) # pack expected 3 items for packing (got 4)
此外,我們之前說(shuō)一個(gè)長(zhǎng)度為 12 的字節(jié)串,可以使用 12s 來(lái)表示,那么 3s 就表示長(zhǎng)度為 3 的字節(jié)串。問(wèn)題來(lái)了,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 個(gè)值, 但是只傳遞了兩個(gè) packed_data = struct.pack("3i", 1, 2, 3) print(struct.unpack("3i", packed_data)) # (1, 2, 3)
我們看到 3i 在結(jié)果上等同于 iii,但對(duì)于 s 而言,3s 可不等同于 sss。3s 仍然表示接收一個(gè)元素,只不過(guò)這個(gè)元素是字節(jié)串,并且長(zhǎng)度為 3。這些細(xì)節(jié)要注意。 當(dāng)然對(duì)于字符串而言,即使長(zhǎng)度不一樣也是無(wú)所謂的,我們舉個(gè)例子。 import struct
# 第一個(gè)值是整數(shù), 第二個(gè)值是字節(jié)串(長(zhǎng)度應(yīng)該為3, 但不是3也可以) packed_data = struct.pack("i3s", 6, b"abcdefg") print(packed_data) # b'\x06\x00\x00\x00abc' # 我們看到被截?cái)嗔? 只剩下了 abc
packed_data = struct.pack("i6s", 6, b"abc") print(packed_data) # b'\x06\x00\x00\x00abc\x00\x00\x00' # 6s 需要字節(jié)長(zhǎng)度為 6, 但是我們只有三個(gè), 所以在結(jié)尾補(bǔ)上了 3 個(gè) \0
總之在使用 struct 進(jìn)行打包的時(shí)候,需要記住兩點(diǎn): 元素個(gè)數(shù)和符號(hào)個(gè)數(shù)要對(duì)應(yīng), 比如 3i3s3f 表示接收 7 個(gè)元素,依次是 3 個(gè)整數(shù)、一個(gè)字節(jié)串、3 個(gè)浮點(diǎn)數(shù); 元素類(lèi)型和符號(hào)要對(duì)應(yīng),比如 i 對(duì)應(yīng)整數(shù),s 對(duì)應(yīng)字節(jié)串等等;并且對(duì)于 s 而言,前面的數(shù)字表示接收的字節(jié)串的長(zhǎng)度;
而對(duì)于解包而言,我們也需要關(guān)注,但只需要關(guān)注一點(diǎn),那就是大小。怎么理解呢?來(lái)舉個(gè)例子: import struct
packed_data = struct.pack("ii", 1, 2) print(packed_data) # b'\x01\x00\x00\x00\x02\x00\x00\x00' # 因?yàn)?i 表示 C 的 int, 而 C 的一個(gè) int 占 4 字節(jié), 所以結(jié)果是 8 字節(jié)。 # 只不過(guò) 1 和 2 只需一字節(jié)即可存儲(chǔ), 因此其它的部分都是 0 # 打包之后的 packed_data 的大小, 不取決于打包的元素, 而是取決于格式化字符串中的類(lèi)型符號(hào) # 比如 struct.pack("4s", b"abc") , 盡管傳遞的字節(jié)串只有 3 字節(jié) # 但指定的是 4s, 所以打包之后的 packed_data 占 4 字節(jié)
# 而我們?cè)诮獍臅r(shí)候, 指定的符號(hào)的字節(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 # 告訴我們需要一個(gè) 4 字節(jié)的buffer, 這是因?yàn)槲覀兊?nbsp;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)
那么問(wèn)題來(lái)了,我們說(shuō)一個(gè) long long 是占 8 字節(jié),正好對(duì)應(yīng)兩個(gè) int,那么將兩個(gè) int 按照一個(gè) long long 來(lái)解析可不可以呢?再有 8s 也是 8 字節(jié),又可不可以進(jìn)行解析呢?我們來(lái)試一下。 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,) """
# 答案顯然是可以的, 因?yàn)樽止?jié)數(shù)是相匹配的 # 對(duì)于 8s 而言, 我們看到解析出來(lái)的結(jié)果就是原始的字節(jié)流 # 對(duì)于 q, 也可以正確解析, 只不過(guò)結(jié)果不是我們想要的
# 但是我們觀察一下按照 q 解析出來(lái)的結(jié)果, 結(jié)果是 8589934593, 那么它是怎么得到的呢? # 如果將 packet_data 按照 8 字節(jié)整數(shù)解析,相當(dāng)于將兩個(gè) 4 字節(jié)整數(shù)合并成一個(gè) 8 字節(jié)整數(shù) # 其中整數(shù) 2 占據(jù)前 32 個(gè)位,整數(shù) 1 占據(jù)后 32 個(gè)位 print((2 << 32) + 1) """ 8589934593 """ # 怎么樣, 結(jié)果是不是一樣呢? 至于這里為什么不是 (1 << 31) + 2, 我們后面會(huì)說(shuō)
所以在解析的時(shí)候,格式化字符串中的類(lèi)型符號(hào)對(duì)應(yīng)的字節(jié)數(shù),要和 packed_data 的字節(jié)數(shù)相匹配,這是不報(bào)錯(cuò)的前提。當(dāng)然如果想得到正確的結(jié)果,最關(guān)鍵的還是解包對(duì)應(yīng)的格式化字符串,要和打包對(duì)應(yīng)的格式化字符串保持一致。
在 struct 模塊中,我們可以直接使用 struct.pack 和 struct.unpack 這兩個(gè)模塊級(jí)的函數(shù),但是 struct 模塊還提供了一個(gè) Struct 類(lèi)。 import struct
s = struct.Struct("ii") # 和使用 struct.pack("ii", 1, 2) 之間是等價(jià)的 packed_data = s.pack(1, 2) print(packed_data) """ b'\x01\x00\x00\x00\x02\x00\x00\x00' """
如果我們需要使用同一種格式化字符串來(lái)對(duì)大量數(shù)據(jù)進(jìn)行打包的話,那么使用 struct.Struct 是更推薦的,可以類(lèi)比正則。 re.search(pattern, string) 這個(gè)過(guò)程分為兩步,會(huì)先將 pattern 進(jìn)行編譯轉(zhuǎn)換,然后再進(jìn)行匹配。如果我們需要同一個(gè) pattern 匹配 100 個(gè)字符串的話,那么要編譯轉(zhuǎn)換 100 次。 而如果先對(duì) pattern 進(jìn)行編譯 comp = re.compile(pattern),那么不管調(diào)用 comp.search(string) 多少次,都只會(huì)進(jìn)行一次編譯轉(zhuǎn)換,效率會(huì)更高。struct 也是類(lèi)似的,如果要按照相同的格式進(jìn)行多次打包,那么創(chuàng)建一個(gè) Struct 實(shí)例并在這個(gè)實(shí)例上調(diào)用方法時(shí)(不使用模塊級(jí)函數(shù))會(huì)更高效。 當(dāng)然,使用 Struct 類(lèi)還有一個(gè)好處,就是可以獲取一些額外信息。 import struct
s = struct.Struct("ii4sf") print("格式化字符串:", s.format) # 格式化字符串: ii4sf print("字節(jié)數(shù):", s.size) # 字節(jié)數(shù): 16
我們看到打包后的數(shù)據(jù)大小是由格式化字符串中的符號(hào)所決定的。 說(shuō)到字節(jié)序,你應(yīng)該會(huì)想到大端存儲(chǔ)、小端存儲(chǔ),所謂大端存儲(chǔ)就是:數(shù)據(jù)的低位存儲(chǔ)在內(nèi)存的高地址中,高位存儲(chǔ)在內(nèi)存的低地址中。而小端存儲(chǔ)與之相反:數(shù)據(jù)的低位存儲(chǔ)在內(nèi)存的低地址中,高位存儲(chǔ)在內(nèi)存的高地址中。 那么 Python 的 struct 默認(rèn)使用什么存儲(chǔ)呢?答案是小端存儲(chǔ)。 import struct
# i 表示 int32,那么相應(yīng)的整數(shù) 1 就占 4 字節(jié) # 其中最低位存儲(chǔ)的是 1,剩余三個(gè)位存儲(chǔ)的是 0 packed_data = struct.pack("i", 1) print(packed_data) # b'\x01\x00\x00\x00' # 打包之后的數(shù)據(jù)是一個(gè)字節(jié)串,或者理解為 C 的字符數(shù)組 # 而數(shù)組的元素,從左往右對(duì)應(yīng)的內(nèi)存地址是依次增大的 # 所以結(jié)果就是低位存在了低地址中,所以這里是小端存儲(chǔ)
# 而如果想要變成大端存儲(chǔ)的話, 可以這么做 packed_data = struct.pack(">i", 1) print(packed_data) # b'\x00\x00\x00\x01' # 我們看到此時(shí)結(jié)果就變了
當(dāng)然我們?cè)诮馕龅臅r(shí)候也需要注意大小端的問(wèn)題,如果是打包的時(shí)候使用的是大端存儲(chǔ),那么解包的時(shí)候也要使用大端存儲(chǔ)。 import struct
# 因?yàn)?nbsp;\x01 在最后面,而后面表示內(nèi)存的高地址 # 此時(shí)這個(gè)數(shù)字如果想表示 1, 那么它一定是大端存儲(chǔ)的 1 # 也就是將低位的 \x01 放在了高地址中 packed_data = b'\x00\x00\x00\x01'
# 這里我們不指定大小端, 默認(rèn)是小端 print(struct.unpack("i", packed_data)) """ (16777216,) """ # 我們看到結(jié)果變了, 至于這個(gè)結(jié)果怎么來(lái)的, 很簡(jiǎn)單 # 無(wú)論打包還是解包,如果不指定字節(jié)序,那么默認(rèn)都是小端,低位存在低地址中 # 所以 b'\x00\x00\x00\x01' 等價(jià)于如下 print(0b00000001_00000000_00000000_00000000) """ 16777216 """
# 所以我們也要用大端存儲(chǔ)進(jìn)行解析, 表示: 我是大端存儲(chǔ), 存儲(chǔ)在高地址的是數(shù)據(jù)的低位 print(struct.unpack(">i", packed_data)) """ (1,) """ print(0b00000000_00000000_00000000_00000001) """ 1 """ # 所以通過(guò) b'\x00\x00\x00\x01' 的值是不是 1,可以判斷當(dāng)前采用的是大端還是小端 # 因?yàn)?\x01 在高地址,如果值為 1,說(shuō)明 \x01 是低位,因此是大端,否則小端
然后再回顧一下之前的例子,我們用一個(gè) long long 表示兩個(gè) int。 import struct
packed_data = struct.pack("ii", 1, 2) print(packed_data) """ b'\x01\x00\x00\x00\x02\x00\x00\x00' """
# 將兩個(gè) int 轉(zhuǎn)成一個(gè) long long,默認(rèn)是小端,所以低位存在低地址中 # 因此 \x01\x00\x00\x00 表示 1,占據(jù)低 32 個(gè)位 # \x02\x00\x00\x00 表示 2,占據(jù)高 32 個(gè)位 print(struct.unpack("q", packed_data)) print((2 << 32) + 1) """ (8589934593,) 8589934593 """
# 如果按照大端解析的話,低位存在高地址中,那么就是相反的 # 但此時(shí) \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é)序的功能,可以顯式地指定是使用大端存儲(chǔ)、還是小端存儲(chǔ)。而方法也很簡(jiǎn)單,只需要給格式化字符串的第一個(gè)字符指定為特定的符號(hào)即可實(shí)現(xiàn)這一點(diǎn)。 > : 大端字節(jié)序(Big-endian)
< : 小端字節(jié)序(Little-endian)
! : 網(wǎng)絡(luò)字節(jié)序(實(shí)際上就是大端字節(jié)序)
= : 本地字節(jié)序(當(dāng)前系統(tǒng)的字節(jié)序)
@ : 本地字節(jié)序(和 = 相同,但可能有不同的對(duì)齊方式)
pack 方法在打包的時(shí)候,會(huì)為打包數(shù)據(jù)申請(qǐng)一塊內(nèi)存空間,也就是說(shuō)每一次 pack 都需要申請(qǐng)內(nèi)存資源,顯然這是一種浪費(fèi)。通過(guò)避免為每個(gè)打包數(shù)據(jù)分配一個(gè)新緩沖區(qū),在內(nèi)存開(kāi)銷(xiāo)上可以得到優(yōu)化。而 pack_into 和 pack_from 可以支持我們從指定的緩沖區(qū)進(jìn)行讀取和寫(xiě)入。 import struct import ctypes
# 創(chuàng)建一個(gè) string 緩存, 大小為 10 buf = ctypes.create_string_buffer(10) # raw 表示原始數(shù)據(jù),這里都是 \0,因?yàn)?nbsp;C 中是通過(guò) \0 來(lái)標(biāo)識(shí)一個(gè)字符串的結(jié)束 print(buf.raw) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # value 就是 Python 中的字符串, 顯然為空 print(buf.value) # b''
# 然后我們進(jìn)行打包, 第二個(gè)參數(shù)表示緩沖區(qū) # 第三個(gè)參數(shù)表示偏移量, 0表示從頭開(kāi)始寫(xiě)入; 然后后面的參數(shù)就是打包的數(shù)據(jù) struct.pack_into("ii2s", buf, 0, 123, 345, b"ab")
# 打包之后的數(shù)據(jù)會(huì)存在 buf 中,解包的話,使用 unpack_from # 會(huì)從 buf 中讀取數(shù)據(jù)并解析,第三個(gè)參數(shù)表示從偏移量為 0 的位置開(kāi)始解析 values = struct.unpack_from("ii2s", buf, 0) print(values) # (123, 345, b'ab')
這里的 pack_into 不會(huì)產(chǎn)生新的內(nèi)存空間,都是對(duì) buf 進(jìn)行操作。另外我們還看到了偏移量,所以可以將多個(gè)打包的數(shù)據(jù)寫(xiě)入到同一個(gè) buf 中,然后也可以從同一個(gè) buf 中進(jìn)行解包,而保證數(shù)據(jù)不沖突的前提正是這里的偏移量,舉個(gè)栗子: 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 開(kāi)始解析 print(s2.unpack_from(buf, s1.size)) # (b'gh',) # 從 0 開(kāi)始解析,解析 s1.size 個(gè)字節(jié) print(s1.unpack_from(buf, 0)) # (1, 2, b'abcdef', 3)
以上就是 struct 模塊,它定義了Python中整數(shù)、浮點(diǎn)數(shù)和二進(jìn)制流之間的通用轉(zhuǎn)換邏輯。
|