午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

使用 struct 模塊打包/解包二進(jìn)制數(shù)據(jù)

 古明地覺(jué)O_o 2023-09-12 發(fā)布于北京

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。

  • pack:將數(shù)據(jù)打包成二進(jìn)制字節(jié)流;

  • unpack:對(duì)二進(jìn)制字節(jié)流進(jìn)行解包;

import binascii
import struct

# values 包含一個(gè) 12 字節(jié)的字節(jié)串、一個(gè)整數(shù)、以及一個(gè)浮點(diǎn)數(shù)。
values = ("古明地覺(jué)".encode("utf-8"), 17156.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"123.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"12)
except Exception as e:
    print(e)  # pack expected 3 items for packing (got 2)

try:
    # iii 表示接收 3 個(gè)整數(shù), 但我們卻傳遞了四個(gè)
    struct.pack("iii"1234)
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"12)
except Exception as e:
    print(e)  # pack expected 3 items for packing (got 2)

# 告訴我們接收 3 個(gè)值, 但是只傳遞了兩個(gè)
packed_data = struct.pack("3i"123)
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"6b"abcdefg")
print(packed_data)  # b'\x06\x00\x00\x00abc'
# 我們看到被截?cái)嗔? 只剩下了 abc

packed_data = struct.pack("i6s"6b"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"12)
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"12)
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


在 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(12
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)所決定的。


字節(jié)序


說(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"12)
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)。

  1. > : 大端字節(jié)序(Big-endian)

  2. < : 小端字節(jié)序(Little-endian)

  3. ! : 網(wǎng)絡(luò)字節(jié)序(實(shí)際上就是大端字節(jié)序)

  4. = : 本地字節(jié)序(當(dāng)前系統(tǒng)的字節(jié)序)

  5. @ : 本地字節(jié)序(和 = 相同,但可能有不同的對(duì)齊方式)


緩沖區(qū)


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, 0123345b"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, 012b"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)換邏輯。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多