用 Python 學 Google Protocol Buffers - Part 3
Posted on Nov 9, 2018 in Python 程式設計 - 高階 by Amo Chen ‐ 3 min read
本文為系列教學:
- 用 Python 學 Google Protocol Buffers - Part 1
- 用 Python 學 Google Protocol Buffers - Part 2
- 用 Python 學 Google Protocol Buffers - Part 3
上篇文章介紹 Google Protocols Buffers 介紹 proto3
幾個重要語法與特性,本篇將介紹如何撰寫更複雜的 message ,以及幾種方便的資料型態與其使用方法。
複合式 message
一般來說 message 的定義很可能會像 JSON 字串一樣, 1 個 object 內又包著 1 個 object ,所以也會有 message 內包著其他 message 的情況,例如購物車內可能會包含各種產品:
syntax = "proto3";
message User {
string id = 1;
string name = 2;
}
message Product {
string id = 1;
string name = 2;
int32 price = 3;
int32 quantity = 4;
}
message Cart {
User user = 1;
repeated Product products = 2;
}
複合式的 message 就是先定義最小的 message 之後,再由這些小 message 組合而成。
編譯成 Python 之後就能夠用 MergeFrom
或是 add
(add 限 repeated 的欄位)等方法進行組裝:
>>> from cart_pb2 import User, Product, Cart
>>> user = User()
>>> user.id = "userid"
>>> user.name = "username"
>>>
>>> cart = Cart()
>>> cart.user.MergeFrom(user)
>>> product = cart.products.add()
>>> product.id = "productid"
>>> product.name = "productname"
>>> product.price = 100
>>> product.quantity = 1
>>>
>>> cart.SerializeToString()
b'\n\x12\n\x06userid\x12\x08username\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01'
如果是從 binary 資料想直接組裝的話,則可以使用 MergeFromString
:
>>> product_string = product.SerializeToString()
>>> product_2 = cart.products.add()
>>> product_2.MergeFromString(product_string)
>>> print(len(cart.products))
2
>>> cart.SerializeToString()
b'\n\x12\n\x06userid\x12\x08username\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01'
以上就是複合式 message 的使用方法,是否相當簡單?
import .proto
檔
Google Protocol Buffers 可以將 message 拆成不同的 .proto
檔,再用 import 的方式引入:
syntax "proto3";
import "user.proto";
message Product {
string id = 1;
string name = 2;
int32 price = 3;
int32 quantity = 4;
}
message Cart {
User user = 1;
repeated Product products = 2;
}
上述範例可以看到 User 是透過 import "user.proto";
引入的。
Any 型別
偶爾可能也會遇到 message 欄位可能會存放各式各樣資料的情況,例如日誌類型的資料不見得有固定的格式,這種情況下可以試著使用 proto3 所提供的擴充型別 - Any
。
使用 Any 型別需要透過 import "google/protobuf/any.proto";
引用。
以下範例試著定義 1 個 message type 稱為 Event ,其中欄位 payload
為 Any
,用以裝載各式各樣的 message type:
syntax = "proto3";
import "google/protobuf/any.proto";
message Event {
string event = 1;
google.protobuf.Any payload = 2;
}
以下為操作 Event 的 Python 範例:
>>> from event_pb2 import Event
>>> from cart_pb2 import User
>>>
>>> event = Event()
>>> event.type = "viewProductPage"
>>> user = User()
>>> user.id = "userid"
>>> user.name = "username"
>>> event.payload.Pack(user)
>>> event.payload
type_url: "type.googleapis.com/User"
value: "\n\006userid\022\010username"
上述範例可以看到 Any
型別是用 Pack()
方法將 message 放入欄位之中,如果試圖使用 MergeFrom()
就只能使用同是 Any 型別的 message ,否則會出現類似以下的錯誤:
TypeError: Parameter to MergeFrom() must be instance of same class: expected google.protobuf.Any got User.
用 pack()
裝載之後,如果要解讀 Any 型別的欄位,就得透過 Unpack()
方法:
>>> if event.payload.Is(User.DESCRIPTOR):
... user2 = User()
... event.payload.Unpack(user2)
... print(user2)
id: "userid"
name: "username"
上述範例透過 Is()
方法判斷 payload 欄位是否存放 User
型別的資料,接著新增 1 個 user2 並用 Unpack(user2)
將資料 deserialize 至 user2 。
以上就是 Any
型別的介紹。
其他擴充型別
proto3
新增許多擴充型別,例如 Maps
, Timestamp
, Duration
等等,增加不少實用性與方便性。
以下為 Maps 的宣告格式,需要各自指定 key 與 value 的型別:
map<key_type, value_type> map_field = N;
例如,宣告一個 key 為字串, value 為 User 的 Map:
syntax = "proto3";
message Mapping {
map<string, User> id_user_map = 1;
}
編譯後的使用範例為:
>>> from cart_pb2 import User
>>> from mapping_pb2 import Mapping
>>>
>>> mapping = Mapping()
>>>
>>> user1 = mapping.id_user_map.get_or_create("userid_1")
>>> user1.id = 'userid_1'
>>>
>>> user2 = mapping.id_user_map.get_or_create("userid_2")
>>> user2.id = 'userid_2'
>>>
>>> for k, v in mapping.id_user_map.keys():
... print(k)
...
userid_1
userid_2
上述範例可以看到 map 是透過 get_or_create(key)
新增資料,如果 key 值已存在於 map 中就會取得該值,沒有的話就會新增一組。 map 也可以透過 keys()
走訪已存在的 key 值。
目前其他資料型態的使用方法,在 Google Protocol Buffers 官網上並沒有太多範例,但可以進一步詳閱 protocolbuffers/protobuf Github 中的 .proto
檔,其檔案註解中有使用範例能夠參考。
MessageToJson 將 Message 轉成 JSON
proto3
也增加資料交換的方便性(畢竟許多應用仍使用 JSON 作為資料交換格式),支援將 message 轉成 JSON 字串, 各種資料型態與 JSON 資料型態對照表請參考 JSON Mapping 一表。
以下範例為如何在 Python 中將 message 轉為 JSON:
>>> import google.protobuf.json_format
>>> print(google.protobuf.json_format.MessageToJson(mapping))
{
"idUserMap": {
"userid_2": {
"id": "userid_2"
},
"userid_1": {
"id": "userid_1"
}
}
}
結語
針對 Google Protocol Buffers 介紹就到此完結,希望大家看完此系列文章後,都能夠對 Google Protocol Buffers 有基礎的認識。
References
https://developers.google.com/protocol-buffers/docs/reference/python-generated