詳解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

記得幫我點贊哦!

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

resource-introduce

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

我是職場亮哥,YY高級軟件工程師、四年工作經驗,拒絕咸魚爭當龍頭的斜杠程序員。

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

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

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

wechat-platform-guide-attention

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


免責聲明!

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



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