1. 程式人生 > 實用技巧 >詳解Python Google Protocol Buffer

詳解Python Google Protocol Buffer

本篇主要介紹如何在Python語言中使用Google Protocol Buffer(後續都簡寫為PB), 包括以下幾個部分:

  • 為什麼要使用PB?
  • 安裝Google PB
  • 自定義.proto 檔案
  • 編譯.proto檔案
  • 解析目標py檔案
  • 序列化和反序列化
  • 更復雜的Message
  • 動態編譯

為什麼要使用PB?

PB(Protocol Buffer)是 Google 開發的用於結構化資料交換格式,作為騰訊雲日誌服務標準寫入格式。因此用於寫入日誌資料前,需要將日誌原始資料序列化為 PB 資料流後通過 API 寫入服務端。而各個端類程式中不便操作PB格式,因此需要在端類和日誌服務之間加入一層PB轉化層。

當然PB格式也有自己的優點,主要是簡單和快,具體測試結果參見Google序列化基準分析

安裝Google PB

如果要想在Python中使用PB,需要先安裝PB編譯器protoc去編譯你的.proto檔案,安裝方法如下:

下載最新的protobuf release包安裝即可,當前版本為3.5.1,安裝步驟如下

wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.tar.gz
tar xvfz protobuf-all-3.5.1.tar.gz
cd protobuf-3.5.1/
./configure --prefix=/usr
make
make check
make install

check步驟全部通過即表示編譯通過。

繼續安裝protobuf的python模組

cd ./python 
python setup.py build 
python setup.py test 
python setup.py install

安裝完成驗證protoc命令

root@ubuntu:~# protoc --version
libprotoc 3.5.1

protobuf的預設安裝位置是 /usr/local,/usr/local/lib 不在Ubuntu系統預設的 LD_LIBRARY_PATH 裡,如果在Ubuntu系統中configure時未指定安裝路徑為/usr, 則會出現以下錯誤

protoc: error while loading shared libraries: libprotoc.so.8: cannot open shared object file: No such file or directory

可以使用ldconfig命令解決,參考Protobuf cannot find shared libraries,這個錯誤在安裝包的README中有提到。當然重新安裝也可以

驗證Python模組是否被正確安裝

import google.protobuf

在python直譯器中如果上面的import沒有報錯,說明安裝正常。

自定義.proto 檔案

首先我們需要編寫一個 proto 檔案,定義我們程式中需要處理的結構化資料,在 protobuf 的術語中,結構化資料被稱為 Message。proto 檔案非常類似 java 或者 C++ 語言的資料定義。proto示例檔案cls.Log.proto如下:

syntax = "proto2";
package cls;
message Log
{
    optional uint64 time = 1; // UNIX Time Format
    required string topic_id = 2;
    required string content = 3;
}

.proto檔案開頭是包的宣告,為了幫助防止在不同的工程中命名衝突。在Python中,包通常由目錄結構決定的,所以這個.proto檔案定義的包,在實際Python程式碼中是沒有效果的。但是,按照官方的建議是堅持宣告這條語句,主要作用是為了在PB的名稱空間中防止名稱衝突。package 名字叫做 cls,定義了一個訊息 Log,該訊息有三個成員,各個成員的含義如下:

欄位名 型別 位置 是否必須 含義
time uint64 body 日誌時間,不指定,則使用伺服器收到請求的時間
topic_id string body 日誌上報到的日誌主題id
content string body 日誌內容

一個比較好的習慣是認真對待 proto 檔案的檔名。比如將命名規則定為: packageName.MessageName.proto

編譯.proto檔案

使用編譯器protoc直接編譯即可,需要指定原始檔路徑和目標檔案路徑

SRC_DIR=/tmp/src_dir
DST_DIR=/tmp/dst_dir
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/cls.Log.proto

生成Python類就使用--python_out選項,如果要生成C++類時使用--cpp_out選項

解析目標py檔案

在目標資料夾中生成的檔案目錄對應如下:

root@ubuntu:/tmp/dst_dir# tree
.
└── cls
    └── Log_pb2.py

1 directory, 1 file

其中Log_pb2.py檔案的內容如下(不允許編輯):

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: cls.Log.proto

import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
from google.protobuf import descriptor_pb2
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor.FileDescriptor(
  name='cls.Log.proto',
  package='cls',
  syntax='proto2',
  serialized_pb=_b('\n\rcls.Log.proto\x12\x03\x63ls\"6\n\x03Log\x12\x0c\n\x04time\x18\x01 \x01(\x04\x12\x10\n\x08topic_id\x18\x02 \x02(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x02(\t')
)




_LOG = _descriptor.Descriptor(
  name='Log',
  full_name='cls.Log',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='time', full_name='cls.Log.time', index=0,
      number=1, type=4, cpp_type=4, label=1,
      has_default_value=False, default_value=0,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='topic_id', full_name='cls.Log.topic_id', index=1,
      number=2, type=9, cpp_type=9, label=2,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='content', full_name='cls.Log.content', index=2,
      number=3, type=9, cpp_type=9, label=2,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=22,
  serialized_end=76,
)

DESCRIPTOR.message_types_by_name['Log'] = _LOG
_sym_db.RegisterFileDescriptor(DESCRIPTOR)

Log = _reflection.GeneratedProtocolMessageType('Log', (_message.Message,), dict(
  DESCRIPTOR = _LOG,
  __module__ = 'cls.Log_pb2'
  # @@protoc_insertion_point(class_scope:cls.Log)
  ))
_sym_db.RegisterMessage(Log)


# @@protoc_insertion_point(module_scope)

關於pb生成的py檔案的原始碼的解析暫時擱置,可以參見附件中的資料

序列化和反序列化

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Created on 1/30/18 4:23 PM
@author: Chen Liang
@function: pb test
"""

import sys

reload(sys)
sys.setdefaultencoding('utf-8')
import Log_pb2
import json


def serialize_to_string(msg_obj):
    ret_str = msg_obj.SerializeToString()
    return ret_str


def parse_from_string(s):
    log = Log_pb2.Log()
    log.ParseFromString(s)
    return log

if __name__ == '__main__':
    # serialize_to_string
    content_dict = {"live_id": "1239182389648923", "identify": "zxc_unique"}
    tencent_log = Log_pb2.Log()
    tencent_log.time = 1510109254
    tencent_log.topic_id = "John Doe"
    tencent_log.content = json.dumps(content_dict)
    ret_s = serialize_to_string(tencent_log)
    print(type(ret_s))
    print(ret_s)

    # parse_from_string
    log_obj = parse_from_string(ret_s)
    print(log_obj)

其中關鍵的操作在於message物件的寫入和讀取以及序列化函式SerializeToString和反序列化函式ParseFromString

更復雜的Message

到這裡為止,我們只給出了一個簡單的上傳日誌的例子。在實際應用中,人們往往需要定義更加複雜的 Message。我們用“複雜”這個詞,不僅僅是指從個數上說有更多的 fields 或者更多型別的 fields,而是指更加複雜的資料結構:

  • Message巢狀
  • Import Message

下面分別介紹

Message巢狀

巢狀是一個神奇的概念,一旦擁有巢狀能力,訊息的表達能力就會非常強大。具體的巢狀 Message 的例子如下

message Person { 
 required string name = 1; 
 required int32 id = 2;        // Unique ID number for this person. 
 optional string email = 3; 
 
 enum PhoneType { 
   MOBILE = 0; 
   HOME = 1; 
   WORK = 2; 
 } 
 
 message PhoneNumber { 
   required string number = 1; 
   optional PhoneType type = 2 [default = HOME]; 
 } 
 repeated PhoneNumber phone = 4; 
}

在 Message Person 中,定義了巢狀訊息 PhoneNumber,並用來定義 Person 訊息中的 phone 域。這使得人們可以定義更加複雜的資料結構。

Import Message

在一個 .proto 檔案中,還可以用 Import 關鍵字引入在其他 .proto 檔案中定義的訊息,這可以稱做 Import Message,或者 Dependency Message。具體的import message的例子如下

import common.header; 
 
message youMsg{ 
 required common.info_header header = 1; 
 required string youPrivateData = 2; 
}

其中 ,common.info_header定義在common.header包內。

Import Message 的用處主要在於提供了方便的程式碼管理機制,類似 C 語言中的標頭檔案。您可以將一些公用的 Message 定義在一個 package 中,然後在別的 .proto 檔案中引入該 package,進而使用其中的訊息定義。

Google Protocol Buffer 可以很好地支援巢狀 Message 和引入 Message,從而讓定義複雜的資料結構的工作變得非常輕鬆愉快。

動態編譯

一般情況下,使用 Protobuf 的人們都會先寫好 .proto 檔案,再用 Protobuf 編譯器生成目標語言所需要的原始碼檔案。將這些生成的程式碼和應用程式一起編譯。

可是在某且情況下,人們無法預先知道 .proto 檔案,他們需要動態處理一些未知的 .proto 檔案。比如一個通用的訊息轉發中介軟體,它不可能預知需要處理怎樣的訊息。這需要動態編譯 .proto 檔案,並使用其中的 Message。

詳細解釋參見:Google Protocol Buffer 的使用和原理


參考:

  1. https://developers.google.com/protocol-buffers/docs/reference/python/
  2. https://developers.google.com/protocol-buffers/docs/reference/python-generated
  3. http://hzy3774.iteye.com/blog/2323428
  4. https://github.com/google/protobuf/tree/master/python
  5. https://github.com/google/protobuf/tree/master/examples
  6. https://blog.csdn.net/losophy/article/details/17006573
  7. https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/
  8. https://github.com/google/protobuf
  9. https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.tar.gz
  10. Python Google Protocol Buffer: https://developers.google.com/protocol-buffers/docs/pythontutorial

記得幫我點贊哦!

精心整理了計算機各個方向的從入門、進階、實戰的視訊課程和電子書,按照目錄合理分類,總能找到你需要的學習資料,還在等什麼?快去關注下載吧!!!

念念不忘,必有迴響,小夥伴們幫我點個贊吧,非常感謝。

我是職場亮哥,YY高階軟體工程師、四年工作經驗,拒絕鹹魚爭當龍頭的斜槓程式設計師。

聽我說,進步多,程式人生一把梭

如果有幸能幫到你,請幫我點個【贊】,給個關注,如果能順帶評論給個鼓勵,將不勝感激。

職場亮哥文章列表:更多文章

本人所有文章、回答都與版權保護平臺有合作,著作權歸職場亮哥所有,未經授權,轉載必究!