這裡介紹在 Python 使用 Protocol Buffers 格式來儲存資料的方式,以及實際應用範例。

Protocol Buffers 是一種高效率、高彈性的結構化資料序列儲存格式,其類似 XML,但更省空間、處理效率更好,而且語法也更簡單。在使用 Protocol Buffers 前,我們會先定義資料的結構規則,再使用 Protocol Buffers 的編譯器產生適用於自己專案的 API 程式碼(例如 C++ 或 Python 程式碼等),然後在程式中呼叫這個 API 來存取 Protocol Buffers 格式的資料。


以下是在 Python 程式中使用 Protocol Buffers 的流程。

Protocol 資料格式

首先建立一個 .proto 檔,定義好自己的 Protocol 資料格式:

/* 這是一個簡單的 Protocol 資料格式示範,
   若要在 .proto 檔案中加入註解,
   可使用 C/C++ 的註解語法。              */

// Protocol 資料格式版本
syntax = "proto2";

// 套件名稱
package tutorial;

// 定義資料格式
message Person {
  // 基本資料 
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  // 列舉型別
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  // 定義另外一個 message
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  // 多筆 PhoneNumber 資料
  repeated PhoneNumber phones = 4;
}

message AddressBook {
  // 多筆 Person 資料
  repeated Person people = 1;
}

.proto 檔的語法相當類似 C++ 或 Java 的類別定義。一開始以 package 所定義的套件名稱是為了避免不同專案之間產生名稱衝突,但在 Python 之中都是以目錄結構來區別套件,所以 package 在 Python 中並沒有太多作用(但是在其他程式語言中就會需要,所以還是建議要加這一行)。

在套件名稱定義之後,接著就是主要的資料格式,這裡的 message 就是打包許多資料的容器,一個 message 裡面可以包含各種的資料,例如 布林值(bool)、整數(int32)、單精度浮點數(float)、雙精度浮點數(double)與字串(string),而一個 message 之中也可以包含其他的 message

在這個例子中,我們定義一個名為 Personmessage,其除了包含一些基本資料外,還包含多個名為 PhoneNumbermessage,而最後又定義了一個 AddressBookmessage,其中包含多筆 Personmessage

我們也可以在一個 message 當中又定義另一個 message(就像這裡的PhoneNumber 定義放在 Persion 之中)。

enum 是一種列舉型別,可限制欄位的資料只能是某些預定的值,例如這裡的 PhoneType 的值就只能是 MOBILEHOMEWORK

在定義欄位時,欄位後方所加上的 = 1= 2 等編號是代表欄位的識別標籤(tag),這些標籤在二進位編碼時會被使用到,標籤 115 在編碼時會比較省空間,16 以上的標籤在儲存時會多出一個位元組(byte),通常我們會讓重複出現(repeated)的欄位使用 15 以下的標籤,而不常出現的欄位則使用 16 以上的標籤,達到最佳化的的目的。

每一個欄位都必須用以下其中一種 modifier 來標示該欄位的出現次數:

  • required:必要欄位,不可省略,否則資料解析時就會出錯。
  • optional:選擇性欄位,若省略則會以預設值作為其值。
  • repeated:重複性欄位,表示該欄位可重複出現任何次數,或是完全不出現。在 Protocol Buffers 資料中會保留重複出現資料的順序,就類似陣列的概念。

編譯 Protocol Buffers

建立好 .proto 檔案之後,接著就可以用 Protocol Buffers 的編譯器將 .proto 檔案編譯出可用來存取 AddressBook(以及 PersionPhoneNumber)資料的 API 程式碼。

首先從 GitHub 上下載 Protocol Buffers 的編譯器,並安裝起來。若在 Ubuntu Linux 上,可直接使用 apt 安裝:

sudo apt-get install protobuf-compiler

安裝完後,編譯 .proto 檔案:

# 程式碼所在目錄
SRC_DIR=`pwd`

# 輸出目錄
DST_DIR=`pwd`

# 編譯 .proto 檔
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

這裡我們要編譯出 Python 的 API,所以使用 --python_out 這個參數指定輸出目錄,若要輸出其他程式語言的 API,可以自行修改(例如 --cpp_out--java_out)。

編譯之後,就會產生一個 addressbook_pb2.py Python 指令稿,其中就包含了存取 AddressBook 用的 API。

寫入 Protocol Buffers 資料

產生了存取 AddressBook 用的 API 之後,就可以在自己的 Python 程式中使用,以下是寫入 Protocol Buffers 資料的範例。

import addressbook_pb2
import sys

# 指定 Protocol Buffer 資料檔
my_pb_file = "my_addr_book.pb"

# 建立 AddressBook
address_book = addressbook_pb2.AddressBook()

# 增加一筆 Person 資料
person = address_book.people.add()

# 設定 Person 基本資料
person.id = 123
person.name = "G. T. Wang"
person.email = "guozhao.wang@gmail.com"

# 新增第一筆電話
phone_number = person.phones.add()
phone_number.number = "0912-345678"
phone_number.type = addressbook_pb2.Person.MOBILE

# 新增第二筆電話
phone_number = person.phones.add()
phone_number.number = "06-1234567"
phone_number.type = addressbook_pb2.Person.WORK

# 寫入 AddressBook
with open(my_pb_file, "wb") as f:
  f.write(address_book.SerializeToString())

此程式執行後,就會產生一個 my_addr_book.pb 二進位檔案,其內容就是這裡建立的 AddressBook

讀取 Protocol Buffers 資料

以下是讀取 Protocol Buffers 資料的範例。

import addressbook_pb2
import sys

# 指定 Protocol Buffer 資料檔
my_pb_file = "my_addr_book.pb"

# 建立 AddressBook
address_book = addressbook_pb2.AddressBook()

# 寫入 AddressBook
with open(my_pb_file, "rb") as f:
  address_book.ParseFromString(f.read())

# 顯示資料
for person in address_book.people:
  print("Person ID:", person.id)
  print("  Name:", person.name)
  if person.HasField('email'):
    print("  E-mail address:", person.email)

  for phone_number in person.phones:
    if phone_number.type == addressbook_pb2.Person.MOBILE:
      print("  Mobile phone #:", phone_number.number)
    elif phone_number.type == addressbook_pb2.Person.HOME:
      print("  Home phone #:", phone_number.number)
    elif phone_number.type == addressbook_pb2.Person.WORK:
      print("  Work phone #:", phone_number.number)

此程式執行後,就可以列出上面建立好的 AddressBook 內容:

Person ID: 123
  Name: G. T. Wang
  E-mail address: guozhao.wang@gmail.com
  Mobile phone #: 0912-345678
  Work phone #: 06-1234567

參考資料:Google Developers