Python的基本Protobuf指南(序列化數據)


Python的基本Protobuf指南

協議緩沖區(Protobuf)是Google開發的與語言無關的數據序列化格式。Protobuf之所以出色,原因如下:

  • 數據量低: Protobuf使用二進制格式,該格式比JSON等其他格式更緊湊。
  • 持久性: Protobuf序列化是向后兼容的。這意味着即使接口在此期間發生了更改,您也可以始終還原以前的數據。
  • 按合同設計: Protobuf要求使用顯式標識符和類型來規范消息。
  • gRPC的要求: gRPC(gRPC遠程過程調用)是一種利用Protobuf格式的高效遠程過程調用系統。

就個人而言,我最喜歡Protobuf的是,如果強迫開發人員明確定義應用程序的接口。這是一個改變規則的游戲,因為它使所有利益相關者都能理解界面設計並為之做出貢獻。

在這篇文章中,我想分享我在Python應用程序中使用Protobuf的經驗。

安裝Protobuf

對於大多數系統,Protobuf必須從源代碼安裝。在下面,我描述了Unix系統的安裝:

1.從Git下載最新的Protobuf版本:

wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.4/protobuf-all-3.12.4.tar.gz

  

2.解壓縮

tar -xzf protobuf-all-3.12.4.tar.gz

  

3.安裝:

cd protobuf-3.12.4/ && ./configure && make && sudo make install

  

4.驗證安裝(protoc現在應該可用!)

protoc
protoc --version

  

  

一旦原型編譯器可用,我們就可以開始。

1. Protobuf消息的定義

要使用Protobuf,我們首先需要定義我們要傳輸的消息。消息在.proto文件內定義請考慮官方文檔以獲取協議緩沖區語言的詳細信息。在這里,我僅提供一個簡單的示例,旨在展示最重要的語言功能。

假設我們正在開發一個類似Facebook的社交網絡,該社交網絡完全是關於人及其聯系的。這就是為什么我們要為一個人建模消息。

一個人具有某些固有的特征(例如年齡,性別,身高),還具有我們需要建模的某些外在特征(例如朋友,愛好)。讓我們存儲以下定義src/interfaces/person.proto

syntax = "proto3";

import "generated/person_info.proto";

package persons;

message Person {
    PersonInfo info = 1; // characteristics of the person
    repeated Friend friends = 2; // friends of the person
}

message Friend {
    float friendship_duration = 1; // duration of friendship in days
    repeated string shared_hobbies = 2; // shared interests
    Person person = 3; // identity of the friend
}

 

請注意,我們引用的是另一個原始文件,generated/person_info.proto我們將其定義為:

syntax = "proto3";

package persons;

enum Sex {
    M = 0; // male 
    F = 1; // female
    O = 2; // other
}

message PersonInfo {
    int32 age = 1; // age in years
    Sex sex = 2; 
    int32 height = 3; // height in cm
}

  

不用擔心這些定義對您還沒有意義,我現在將解釋最重要的關鍵字:

  • 語法:語法定義了規范使用哪個版本的Protobuf。我們正在使用proto3
  • import:如果根據另一條消息定義了一條消息,則需要使用import語句將其包括在內您可能想知道為什么導入person.proto我們稍后將對此進行更深入的研究-現在僅知道這是由於Python的導入系統所致。generated/person_info.protointerfaces/person_info.proto
  • package:包定義了屬於同一名稱空間的消息。這樣可以防止名稱沖突。
  • enum:一個枚舉定義一個枚舉類型。
  • messsage:消息是我們想使用Protobuf建模的一條信息。
  • repeatrepeated關鍵字指示一個變量,該變量被解釋為向量。在我們的情況下,friendsFriend消息的向量

還要注意,每個消息屬性都分配有一個唯一的編號。該編號對於協議的向后兼容是必需的:一旦將編號分配給字段,則不應在以后的時間點對其進行修改。

現在我們有了應用程序的基本原型定義,我們可以開始生成相應的Python代碼了。

2.原始文件的編譯

要將原始文件編譯為Python對象,我們將使用Protobuf編譯器protoc

我們將使用以下選項調用原型編譯器:

  • --python_out:將存儲已編譯的Python文件的目錄
  • --proto_path:由於原始文件不在項目的根文件夾中,因此我們需要使用替代文件。通過指定generated=./src/interfaces,編譯器知道在導入其他原始消息時,我們要使用生成文件的路徑(generated),而不是接口的位置(src/interfaces)。

有了這種了解,我們可以像下面這樣編譯原始文件:

mkdir src/generated
protoc src/interfaces/person_info.proto --python_out src/ --proto_path generated=./src/interfaces/
protoc src/interfaces/person.proto --python_out src/ --proto_path generated=./src/interfaces/

  

執行完這些命令后,文件generated/person_pb2.pygenerated/person_info_pb2.py應該存在。例如, person_pb2.py如下所示:

_PERSON = _descriptor.Descriptor(
  name='Person', full_name='persons.Person', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ ...

 

生成的Python代碼並非真正可讀。但這沒關系,因為我們只需要知道person_pb2.py可以用於構造可序列化的Protobuf對象即可。

3. Protobuf對象的序列化

在我們以有意義的方式序列化Protobuf對象之前,我們需要用一些數據填充它。讓我們生成一個有一個朋友的人:

# fill protobuf objects import generated.person_pb2 as person_pb2 import generated.person_info_pb2 as person_info_pb2 ############ # define friend for person of interest ############# friend_info = person_info_pb2.PersonInfo() friend_info.age = 40 friend_info.sex = person_info_pb2.Sex.M friend_info.height = 165 friend_person = person_pb2.Person() friend_person.info.CopyFrom(friend_info) friend_person.friends.extend([]) # no friends :-( ####### # define friendship characteristics ######## friendship = person_pb2.Friend() friendship.friendship_duration = 365.1 friendship.shared_hobbies.extend(["books", "daydreaming", "unicorns"]) friendship.person.CopyFrom(friend_person) ####### # assign the friend to the friend of interest ######### person_info = person_info_pb2.PersonInfo() person_info.age = 30 person_info.sex = person_info_pb2.Sex.M person_info.height = 184 person = person_pb2.Person() person.info.CopyFrom(person_info) person.friends.extend([friendship]) # person with a single friend 

請注意,我們通過直接分配填充了所有瑣碎的數據類型(例如,整數,浮點數和字符串)。僅對於更復雜的數據類型,才需要使用其他一些功能。例如,我們利用extend來填充重復的Protobuf字段並CopyFrom填充Protobuf子消息。

要序列化Protobuf對象,我們可以使用SerializeToString()函數。此外,我們還可以使用以下str()函數將Protobuf對象輸出為人類可讀的字符串

# serialize proto object import os out_dir = "proto_dump" with open(os.path.join(out_dir, "person.pb"), "wb") as f: # binary output f.write(person.SerializeToString()) with open(os.path.join(out_dir, "person.protobuf"), "w") as f: # human-readable output for debugging f.write(str(person)) 

執行完代碼段后,可以在proto_dump/person.protobuf以下位置找到生成的人類可讀的Protobuf消息

info {
  age: 30
  height: 184
}
friends {
  friendship_duration: 365.1000061035156
  shared_hobbies: "books"
  shared_hobbies: "daydreaming"
  shared_hobbies: "unicorns"
  person {
    info {
      age: 40
      height: 165
    }
  }
}

請注意,此人的信息既不顯示該人的性別,也不顯示其朋友的性別。這不是Bug,而是Protobuf的功能:0永遠不會打印值為的條目sex由於這兩個人都是男性,因此此處未顯示0

4.自動化的Protobuf編譯

在開發過程中,每次更改后必須重新編譯原始文件可能會變得很乏味。要在安裝開發Python軟件包時自動編譯原始文件,我們可以使用該setup.py腳本。

讓我們創建一個函數,該函數為.proto目錄中的所有文件生成Protobuf代碼src/interfaces並將其存儲在下src/generated

import pathlib import os from subprocess import check_call def generate_proto_code(): proto_interface_dir = "./src/interfaces" generated_src_dir = "./src/generated/" out_folder = "src" if not os.path.exists(generated_src_dir): os.mkdir(generated_src_dir) proto_it = pathlib.Path().glob(proto_interface_dir + "/**/*") proto_path = "generated=" + proto_interface_dir protos = [str(proto) for proto in proto_it if proto.is_file()] check_call(["protoc"] + protos + ["--python_out", out_folder, "--proto_path", proto_path]) 

接下來,我們需要覆蓋develop命令,以便每次安裝軟件包時都調用該函數:

from setuptools.command.develop import develop from setuptools import setup, find_packages class CustomDevelopCommand(develop): """Wrapper for custom commands to run before package installation.""" uninstall = False def run(self): develop.run(self) def install_for_development(self): develop.install_for_development(self) generate_proto_code() setup( name='testpkg', version='1.0.0', package_dir={'': 'src'}, cmdclass={ 'develop': CustomDevelopCommand, # used for pip install -e ./ }, packages=find_packages(where='src') ) 

下次我們運行時pip install -e ./,Protobuf文件將在中自動生成src/generated

我們節省多少空間?

之前,我提到Protobuf的優點之一是其二進制格式。在這里,我們將通過比較Protobuf消息的大小和Person相應的JSON來考慮此優勢

"person": { "info": { "age": 30, "height": 184 }, "friends": { "friendship_duration": 365.1000061035156, "shared_hobbies": ["books", "daydreaming", "unicorns"], "person": { "info": { "age": 40, "height": 165 } } } } 

比較JSON和Protobuf文本表示形式,結果發現JSON實際上更緊湊,因為它的列表表示形式更加簡潔。但是,這令人產生誤解,因為我們對二進制Protobuf格式感興趣。

當比較Person對象的二進制Protobuf和JSON占用的字節數時,我們發現以下內容:

du -b person.pb
53      person.pb

du -b person.json 
304     person.json

在這里,Protobuf比JSON小5倍

二進制Protobuf(53個字節)比相應的JSON(304個字節)小5倍以上。請注意,如果我們使用gRPC協議傳輸二進制Protobuf,則只能達到此壓縮級別。

如果不選擇gRPC,則常見的模式是使用base64編碼對二進制Protobuf數據進行編碼。盡管此編碼不可撤銷地將有效載荷的大小增加了33%,但仍比相應的REST有效載荷小得多。

概要

Protobuf是數據序列化的理想格式。它比JSON小得多,並且允許接口的顯式定義。由於其良好的性能,我建議在所有使用足夠復雜數據的項目中使用Protobuf。盡管Protobuf需要初步的時間投入,但我敢肯定它會很快得到回報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM