欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python中應(yīng)用protobuf的示例詳解

 更新時間:2023年02月19日 08:42:45   作者:古明地覺  
這篇文章主要來和大家聊一聊?protobuf,它是一個數(shù)據(jù)序列化和反序列化協(xié)議,因此它和?json?的定位是一樣的。文中的示例代碼講解詳細,感興趣的可以了解一下

楔子

本次我們來聊一聊 protobuf,它是一個數(shù)據(jù)序列化和反序列化協(xié)議,因此它和 json 的定位是一樣的。當(dāng)客戶端需要傳遞數(shù)據(jù)給服務(wù)端時,會將內(nèi)存中的對象序列化成一個可以在網(wǎng)絡(luò)中傳輸?shù)亩M制流,服務(wù)端收到之后再反序列化,得到內(nèi)存中的對象。

不過既然都有 json 了,還會出現(xiàn) protobuf,那就說明 protobuf 相較于 json 有著很大的優(yōu)勢。來看一下優(yōu)缺點:

總結(jié)一下,protobuf 全稱為 Protocol Buffer,它是 Google 開發(fā)的一種輕量并且高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,性能要遠遠優(yōu)于 json 和 xml。另外 protobuf 經(jīng)歷了兩個版本,分別是 protobuf2 和 protobuf3,目前主流的版本是 3,因為更加易用。

下面就來開始學(xué)習(xí) protobuf 吧。

但是別忘記安裝,直接 pip3 install grpcio grpcio-tools protobuf 即可

編寫一個簡單的 protobuf 文件

protobuf 文件有自己的語法格式,所以相比 json 它的門檻要高一些。我們創(chuàng)建一個文件,文件名為 girl.proto。

protobuf 文件的后綴是 .proto

//?syntax?負責(zé)指定使用哪一種?protobuf?服務(wù)
//?注意:syntax 必須寫在非注釋的第一行
syntax?=?"proto3";

//?包名,?這個目前不是很重要,?你刪掉也是無所謂的
package?girl;

//?把 UserInfo 當(dāng)成 Python 中的類
//?name 和 age 當(dāng)成綁定在實例上的兩個屬性
message?UserInfo?{
??string?name?=?1;??//?=?1表示第1個參數(shù)
??int32?age?=?2;
}

protobuf 文件編寫完成,然后我們要用它生成相應(yīng)的 Python 文件,命令如下:

我們要用 protobuf 文件生成 Python 文件,所以 --python_out 負責(zé)指定 Python 文件的輸出路徑,這里是當(dāng)前目錄;-I 表示從哪里尋找 protobuf 文件,這里也是當(dāng)前目錄;最后的 girl.proto 就是指定的 protobuf 文件了。

我們執(zhí)行該命令,會發(fā)現(xiàn)執(zhí)行完之后多了一個 girl_pb2.py,我們直接用即可。注意:這是基于 protobuf 自動生成的 Python 文件,我們不要修改它。如果參數(shù)或返回值需要改變,那么應(yīng)該修改 protobuf 文件,然后重新生成 Python 文件。

然后我們來看看采用 protobuf 協(xié)議序列化之后的結(jié)果是什么,不是說它比較高效嗎?那么怎能不看看它序列化之后的結(jié)果呢,以及它和 json 又有什么不一樣呢?

import?orjson
import?girl_pb2

#?在?protobuf?文件中定義了?message?UserInfo
#?那么我們可以直接實例化它,而參數(shù)則是?name?和?age
#?因為在?message?UserInfo?里面指定的字段是?name?和?age
user_info?=?girl_pb2.UserInfo(name="satori",?age=17)

#?如果不使用?protobuf,那么我們會選擇創(chuàng)建一個字典
user_info2?=?{"name":?"satori",?"age":?17}

#?然后來看看序列化之后的結(jié)果
#?調(diào)用?SerializeToString?方法會得到序列化之后的字節(jié)串
print(user_info.SerializeToString())
"""
b'\n\x06satori\x10\x11'
"""
#?如果是?json?的話
print(orjson.dumps(user_info2))
"""
b'{"name":"satori","age":17}'
"""

可以看到使用 protobuf 協(xié)議序列化之后的結(jié)果要比 json 短,平均能得到一倍的壓縮。序列化我們知道了,那么如何反序列化呢?

import?orjson
import?girl_pb2

#?依舊是實例化一個對象,但是不需要傳參
user_info?=?girl_pb2.UserInfo()
#?傳入序列化之后的字節(jié)串,進行解析(反序列化)
user_info.ParseFromString(b'\n\x06satori\x10\x11')
print(user_info.name)??#?satori
print(user_info.age)??#?17

#?json?也是同理,通過 loads 方法反序列化
user_info2?=?orjson.loads(b'{"name":"satori","age":17}')
print(user_info2["name"])??#?satori
print(user_info2["age"])??#?17

所以無論是 protobuf 還是 json,都是將一個對象序列化成二進制字節(jié)串。然后根據(jù)序列化之后的字節(jié)串,再反序列出原來的對象。只不過采用 protobuf 協(xié)議進行序列化和反序列化,速度會更快,并且序列化之后的數(shù)據(jù)壓縮比更高,在傳輸?shù)臅r候耗時也會更少。

然后還有一個關(guān)鍵地方的就是,json 這種數(shù)據(jù)結(jié)構(gòu)比較松散。你在返回 json 的時候,需要告訴調(diào)用你接口的人,返回的 json 里面都包含哪些字段,以及類型是什么。但 protobuf 則不需要,因為字段有哪些、以及相應(yīng)的類型,都必須在文件里面定義好。別人只要拿到 .proto 文件,就知道你要返回什么樣的數(shù)據(jù)了,一目了然。

在服務(wù)端之間傳輸 protobuf

如果兩個服務(wù)需要彼此訪問,那么最簡單的方式就是暴露一個 HTTP 接口,服務(wù)之間發(fā)送 HTTP 請求即可彼此訪問,至于請求數(shù)據(jù)和響應(yīng)數(shù)據(jù),則使用 JSON。

所以通過 HTTP + JSON 是最簡單的方式,也是業(yè)界使用最多的方式。但這種方式的性能不夠好,如果是同一個內(nèi)網(wǎng)的多個服務(wù),那么更推薦使用 gRPC + protobuf。關(guān)于 gRPC 以后再聊,我們來看看 protobuf 數(shù)據(jù)在 HTTP 請求中是如何傳遞的。

首先還是編寫 .proto 文件。

//?文件名:girl.proto
syntax?=?"proto3";

package?girl;


message?Request?{
????string?name?=?1;??
????int32?age?=?2;
}

message?Response?{
????string?info?=?1;
}

一個 protobuf 文件中可以定義任意個 message,在生成 Python 文件之后每個 message 會對應(yīng)一個同名的類。然后我們執(zhí)行之前的命令,生成 Python 文件。

接下來使用 Tornado 編寫一個服務(wù):

from?abc?import?ABC
from?tornado?import?web,?ioloop
import?girl_pb2


class?GetInfoHandler(web.RequestHandler,?ABC):

????async?def?post(self):
????????#?拿到客戶端傳遞的字節(jié)流
????????#?這個字節(jié)流應(yīng)該是由?girl_pb2.Request()?序列化得到的
????????content?=?self.request.body
????????#?下面進行反序列化
????????request?=?girl_pb2.Request()
????????request.ParseFromString(content)
????????#?獲取里面的?name?和?age?字段的值
????????name?=?request.name
????????age?=?request.age
????????#?生成?Response?對象
????????response?=?girl_pb2.Response(
????????????info=f"name:?{name},?age:?{age}"
????????)
????????#?但?Response?對象不能直接返回,需要序列化
????????return?await?self.finish(response.SerializeToString())


app?=?web.Application(
????[("/get_info",?GetInfoHandler)]
)
app.listen(9000)

ioloop.IOLoop.current().start()

整個過程很簡單,和 JSON 是一樣的。然后我們來訪問一下:

import?requests
import?girl_pb2

#?往?localhost:9000?發(fā)請求
#?參數(shù)是?girl_pb2.Request()?序列化后的字節(jié)流
payload?=?girl_pb2.Request(
????name="古明地覺",?age=17
).SerializeToString()

#?發(fā)送?HTTP?請求,返回?girl_pb2.Response()?序列化后的字節(jié)流
content?=?requests.post("http://localhost:9000/get_info",
????????????????????????data=payload).content
#?然后我們反序列化
response?=?girl_pb2.Response()
response.ParseFromString(content)
print(response.info)
"""
name:?古明地覺,?age:?17
"""

所以 protobuf 本質(zhì)上也是一個序列化和反序列化協(xié)議,在使用上和 JSON 沒有太大區(qū)別。只不過 JSON 對應(yīng)的 Python 對象是字典,而 protobuf 則是單獨生成的對象。

protobuf 的基礎(chǔ)數(shù)據(jù)類型

在不涉及 gRPC 的時候,protobuf 文件是非常簡單的,你需要返回啥結(jié)構(gòu),那么直接在 .proto 文件里面使用標識符 message 定義即可。

message 消息名稱 {
    類型 字段名 = 1;
    類型 字段名 = 2;
    類型 字段名 = 3;
}

但是類型我們需要說一下,之前用到了兩個基礎(chǔ)類型,分別是 string 和 int32,那么除了這兩個還有哪些類型呢?

以上是基礎(chǔ)類型,當(dāng)然還有復(fù)合類型,我們一會單獨說,先來演示一下基礎(chǔ)類型。編寫 .proto 文件:

//?文件名:basic_type.proto
syntax?=?"proto3";

package?basic_type;

message?BasicType?{
????//?字段的名稱可以和類型名稱一致,這里為了清晰
????//?我們就直接將類型的名稱用作字段名
????int32?int32?=?1;
????sint32?sint32?=?2;
????uint32?uint32?=?3;
????fixed32?fixed32?=?4;
????sfixed32?sfixed32?=?5;

????int64?int64?=?6;
????sint64?sint64?=?7;
????uint64?uint64?=?8;
????fixed64?fixed64?=?9;
????sfixed64?sfixed64?=?10;
????double?double?=?11;
????float?float?=?12;

????bool?bool?=?13;
????string?string?=?14;
????bytes?bytes?=?15;
}

然后我們來生成 Python 文件,命令如下:

python3 -m grpc_tools.protoc --python_out=. -I=. basic_type.proto

執(zhí)行之后,會生成 basic_type_pb2.py 文件,我們測試一下:

import?basic_type_pb2

basic_type?=?basic_type_pb2.BasicType(
????int32=123,
????sint32=234,
????uint32=345,
????fixed32=456,
????sfixed32=789,

????int64=1230,
????sint64=2340,
????uint64=3450,
????fixed64=4560,
????sfixed64=7890,

????double=3.1415926,
????float=2.71,

????bool=True,
????string="古明地覺",
????bytes=b"satori",
)

#?定義一個函數(shù),接收序列化之后的字節(jié)流
def?parse(content:?bytes):
????obj?=?basic_type_pb2.BasicType()
????#?反序列化
????obj.ParseFromString(content)
????print(obj.int32)
????print(obj.sfixed64)
????print(obj.string)
????print(obj.bytes)
????print(obj.bool)

parse(basic_type.SerializeToString())
"""
123
7890
古明地覺
b'satori'
True
"""

很簡單,沒有任何問題,以上就是 protobuf 的基礎(chǔ)類型。然后再來看看符合類型,以及一些特殊類型。

repeat 和 map

repeat 和 map 是一種復(fù)合類型,可以把它們當(dāng)成 Python 的列表和字典。

//?文件名:girl.proto
syntax?=?"proto3";

package?girl;

message?UserInfo?{
????//?對于?Python?而言
????//?repeated?表示?hobby?字段的類型是列表
????//?string?則表示列表里面的元素必須都是字符串
????repeated?string?hobby?=?1;???
????//?map<string,?string>?表示?info?字段的類型是字典
????//?字典的鍵值對必須都是字符串
????map<string,?string>?info?=?2;
}

我們執(zhí)行命令,生成 Python 文件,然后導(dǎo)入測試一下。

import?girl_pb2

user_info?=?girl_pb2.UserInfo(
????hobby=["唱",?"跳",?"rap",?"??"],
????info={"name":?"古明地覺",?"age":?"17"}
)

print(user_info.hobby)
print(user_info.info)
"""
['唱',?'跳',?'rap',?'??']
{'name':?'古明地覺',?'age':?'17'}
"""

結(jié)果正常,沒有問題。但需要注意:對于復(fù)合類型而言,在使用的時候有一個坑。

import?girl_pb2

#?如果我們沒有給字段傳值,那么會有一個默認的零值
user_info?=?girl_pb2.UserInfo()

print(user_info.hobby)??#?[]
print(user_info.info)??#?{}

#?對于復(fù)合類型的字段來說,我們不能單獨賦值
try:
????user_info.hobby?=?["唱",?"跳",?"rap",?"??"]
except?AttributeError?as?e:
????print(e)
"""
Assignment?not?allowed?to?repeated?field?"hobby"?in?protocol?message?object.
"""

#?先實例化,然后單獨給字段賦值,只適用于基礎(chǔ)類型
#?因此我們需要這么做
user_info.hobby.extend(["唱",?"跳",?"rap",?"??"])
user_info.info.update({"name":?"古明地覺",?"age":?"17"})
print(user_info.hobby)
print(user_info.info)
"""
['唱',?'跳',?'rap',?'??']
{'name':?'古明地覺',?'age':?'17'}
"""

所以這算是一個需要注意的點,也不能叫坑吧,總之注意一下即可。

message 的嵌套

通過標識符 message 即可定義一個消息體,大括號里面的則是參數(shù),但參數(shù)的類型也可以是另一個 message。換句話說,message 是可以嵌套的。

//?文件名:girl.proto
syntax?=?"proto3";

package?girl;

message?UserInfo?{
????repeated?string?hobby?=?1;
????//?BasicInfo?定義在外面也是可以的
????message?BasicInfo?{
????????string?name?=?1;
????????int32?age?=?2;
????????string?address?=?3;
????}
????BasicInfo?basic_info?=?2;
}

生成 Python 文件,導(dǎo)入測試一下。

import?girl_pb2

#?在?protobuf?文件中,BasicInfo?定義在?UserInfo?里面
#?所以?BasicInfo?在這里對應(yīng)?UserInfo?的一個類屬性
#?如果定義在全局,那么直接通過?girl_pb2?獲取即可
basic_info?=?girl_pb2.UserInfo.BasicInfo(
????name="古明地覺",?age=17,?address="地靈殿")

user_info?=?girl_pb2.UserInfo(
????hobby=['唱',?'跳',?'rap',?'??'],
????basic_info=basic_info
)

print(user_info.hobby)
"""
['唱',?'跳',?'rap',?'??']
"""
print(user_info.basic_info.name)
print(user_info.basic_info.age)
print(user_info.basic_info.address)
"""
古明地覺
17
地靈殿
"""

以上是 message 的嵌套,或者說通過 message 定義的消息體,也可以作為字段的類型。

枚舉類型

再來聊一聊枚舉類型,它通過 enum 標識符定義。

//?里面定義了兩個成員,分別是?MALE?和?FEMALE
enum?Gender?{
????MALE?=?0;
????FEMALE?=?1;
}

這里需要說明的是,對于枚舉來說,等號后面的值表示成員的值。比如一個字段的類型是 Gender,那么在給該字段賦值的時候,要么傳 0 要么傳 1。因為枚舉 Gender 里面只有兩個成員,分別代表 0 和 1。

而我們前面使用 message 定義消息體的時候,每個字段后面跟著的值則代表序號,從 1 開始,依次遞增。至于為什么要有這個序號,是因為我們在實例化的時候,可以只給指定的部分字段賦值,沒有賦值的字段則使用對應(yīng)類型的零值。那么另一端在拿到字節(jié)流的時候,怎么知道哪些字段被賦了值,哪些字段沒有被賦值呢?顯然要通過序號來進行判斷。

下面來編寫 .proto 文件。

//?文件名:girl.proto
syntax?=?"proto3";

package?girl;

//?枚舉成員的值必須是整數(shù)
enum?Gender?{
????MALE?=?0;
????FEMALE?=?1;
}

message?UserInfo?{
????string?name?=?1;
????int32?age?=?2;
????Gender?gender?=?3;
}

message?Girls?{
????//?列表里面的類型也可以是?message?定義的消息體
????repeated?UserInfo?girls?=?1;
}

輸入命令生成 Python 文件,然后導(dǎo)入測試:

import?girl_pb2

user_info1?=?girl_pb2.UserInfo(
????name="古明地覺",?age=17,
????gender=girl_pb2.Gender.Value("FEMALE"))

user_info2?=?girl_pb2.UserInfo(
????name="芙蘭朵露",?age=400,
????#?傳入一個具體的值也是可以的
????gender=1)

girls?=?girl_pb2.Girls(girls=[user_info1,?user_info2])
print(girls.girls[0].name,?girls.girls[1].name)
print(girls.girls[0].age,?girls.girls[1].age)
print(girls.girls[0].gender,?girls.girls[1].gender)
"""
古明地覺?芙蘭朵露
17?400
1?1
"""

枚舉既可以定義在全局,也可以定義在某個 message 里面。

.proto 文件的導(dǎo)入

.proto 文件也可以互相導(dǎo)入,我們舉個例子。下面定義兩個文件,一個是 people.proto,另一個是 girl.proto,然后在 girl.proto 里面導(dǎo)入 people.proto。

/*?文件名:people.proto */

syntax?=?"proto3";
//?此時的包名就很重要了,當(dāng)該文件被其它文件導(dǎo)入時
//?需要通過這里的包名,來獲取內(nèi)部的消息體、枚舉等數(shù)據(jù)
package?people;

message?BasicInfo?{
????string?name?=?1;
????int32?age?=?2;
}


/*?文件名:girl.proto */
syntax?=?"proto3";
//?導(dǎo)入?people.proto,
import?"people.proto";

message?PersonalInfo?{
????string?phone?=?1;
????string?address?=?2;
}

message?Girl?{
????//?這里的?BasicInfo?是在?people.proto?里面定義的
????//?people.proto?里面的?package?指定的包名為?people
????//?所以這里需要通過?people.?的方式獲取
????people.BasicInfo?basic_info?=?1;
????PersonalInfo?personal_info?=?2;
}

然后執(zhí)行命令,基于 proto 文件生成 Python 文件,顯然此時會有兩個 Python 文件。

python3 -m grpc_tools.protoc --python_out=. -I=. girl.proto 

python3 -m grpc_tools.protoc --python_out=. -I=. people.proto

import?girl_pb2
import?people_pb2

basic_info?=?people_pb2.BasicInfo(name="古明地覺",?age=17)
personal_info?=?girl_pb2.PersonalInfo(phone="18838888888",
??????????????????????????????????????address="地靈殿")
girl?=?girl_pb2.Girl(basic_info=basic_info,
?????????????????????personal_info=personal_info)
print(girl.basic_info.name)??#?古明地覺
print(girl.basic_info.age)??#?17
print(girl.personal_info.phone)??#?18838888888
print(girl.personal_info.address)??#?地靈殿

以上就是 proto 文件的導(dǎo)入,不復(fù)雜。

一些常用的方法

.proto 文件在生成 .py 文件之后,里面的一個消息體對應(yīng)一個類,我們可以對類進行實例化。而這些實例化的對象都有哪些方法呢?我們總結(jié)一下常用的。

首先重新編寫 girl.proto,然后生成 Python 文件。

syntax?=?"proto3";

message?People?{
????string?name?=?1;
????int32?age?=?2;
}

message?Girl?{
????People?people?=?1;
????string?address?=?2;
????int32?length?=?3;
}

內(nèi)容很簡單,我們測試一下。

import?girl_pb2

girl?=?girl_pb2.Girl(
????people=girl_pb2.People(name="古明地覺",?age=17),
????address="地靈殿",
????length=152
)

# SerializeToString:將對象序列化成二進制字節(jié)串
content?=?girl.SerializeToString()

# ParseFromString:將二進制字節(jié)串反序列化成對象
girl2?=?girl_pb2.Girl()
girl2.ParseFromString(content)
print(
????girl2.people.name,
????girl2.people.age,
????girl2.address,
????girl2.length
)??#?古明地覺?17?地靈殿?152

#?以上兩個是最常用的方法

# MergeFrom:將一個對象合并到另一個對象上面
girl?=?girl_pb2.Girl(address="紅魔館",?length=145)
#?我們先實例化了?Girl,后實例化?People
#?接下來要將它綁定到?girl?的?people?字段上
people?=?girl_pb2.People(name="芙蘭朵露",?age=400)
#?但?girl.people?=?people?是會報錯的,因為只有標量才能這么做
#?所以我們可以通過?girl.people.xxx?=?people.xxx?進行綁定
#?但如果?people?的字段非常多,那么會很麻煩
#?因此這個時候可以使用?MergeFrom
girl.people.MergeFrom(people)
print(
????girl.people.name,?girl.people.age
)??#?芙蘭朵露?400

#?同理還有?MergeFromString,接收的是序列化之后的字節(jié)串
people.name,?people.age?=?"魔理沙",?15
girl.people.MergeFromString(people.SerializeToString())
print(
????girl.people.name,?girl.people.age
)??#?魔理沙?15

非常簡單,但我們發(fā)現(xiàn)還少了點什么,就是它和 Python 的字典能不能互相轉(zhuǎn)化呢?答案是可以的,但需要導(dǎo)入專門的函數(shù)。

from?google.protobuf.json_format?import?(
????MessageToJson,
????MessageToDict
)
import?girl_pb2

girl?=?girl_pb2.Girl(
????people=girl_pb2.People(name="古明地覺",?age=17),
????address="地靈殿",
????length=152
)

#?轉(zhuǎn)成?JSON
print(MessageToJson(girl))
"""
{
??"people":?{
????"name":?"\u53e4\u660e\u5730\u89c9",
????"age":?17
??},
??"address":?"\u5730\u7075\u6bbf",
??"length":?152
}
"""

#?轉(zhuǎn)成字典
print(MessageToDict(girl))
"""
{'people':?{'name':?'古明地覺',?'age':?17},?
?'address':?'地靈殿',?'length':?152}
"""

同理,如果我們有一個字典,也可以轉(zhuǎn)成相應(yīng)的對象。

from?google.protobuf.json_format?import?(
????ParseDict
)
import?girl_pb2

data?=?{'people':?{'name':?'魔理沙',?'age':?16},
????????'address':?'魔法森林',?'length':?156}
girl?=?girl_pb2.Girl()
#?基于字典進行解析
ParseDict(data,?girl)
print(girl.people.name)??#?魔理沙
print(girl.people.age)??#?16
print(girl.address)??#?魔法森林
print(girl.length)??#?156

以上就是工作中的一些常用的方法。

小結(jié)

以上就是 protobuf 相關(guān)的內(nèi)容,核心就是編寫 .proto 文件,然后生成 Python 文件。它在業(yè)務(wù)中發(fā)揮的作用,和 json 是類似的,都是將對象轉(zhuǎn)成二進制之后再通過網(wǎng)絡(luò)進行傳輸。接收方在收到字節(jié)流之后,將其反序列化成內(nèi)存中的對象,然后獲取內(nèi)部的字段。

但是 protobuf 比 json 的性能要優(yōu)秀很多,并且通過 .proto 文件定義好結(jié)構(gòu),約束性也要更強一些。

最后補充一點,.proto 文件里面還可以定義很多和 gRPC 相關(guān)的內(nèi)容,關(guān)于 gRPC 我們以后再聊。

到此這篇關(guān)于Python中應(yīng)用protobuf的示例詳解的文章就介紹到這了,更多相關(guān)Python protobuf內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論