Protoco buffers(Python)

发布时间 2023-07-24 11:06:48作者: 一条胖橘猫

Protocol buffers 简称pb,是google开发的一个可序列化、反序列化object的数据交换格式,类似于xml,但是比xml更轻、更快、更简单。而且以上的重点突出一个跨平台,和xml、json等数据序列化一样,跨平台跨语言。

目前已更新到3.x.x版本,正在使用的还有2.x.x版本。

一、特点

1.使用.proto格式文件描述数据层级结构。
2.protobuf编译器会根据.proto文件的描述自动创建一个class编码转换成指定的数据结构,同时生成的类会自动提供 数据转换的getter和setter方法,完成一个protocol buffer的内容的组成或是读写。
3.protobuf格式支持格式扩展兼容,使用旧的proto协议编码仍可以读取使用了新协议编码的数据,当然更新的新协议也是可以兼容之前proto的定义。

二、proto介绍

XXX.proto文件模板举例

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 PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

1. 文件基础

  • .proto文件开头声明使用的语法proto2或proto3:syntax = "proto2"; 如果不声明默认会使用proto2语法
  • .proto文件必须以package xxx;声明开头(语法声明之后位置),作为协议唯一的标识,避免不同的命名冲突
  • Python本身在做关系调用的时候,是作为一种正常的索引结构调用的,也就相当于将编译后的xxx_pb.py结尾的文件认为是普通的python脚本使用,所以在proto中定义的package在编译过程中是没有用到package的,但是避免其他语言使用的时候出现命名冲突,最好还是在文件开头做好声明。

2. message

  • 一个message相当于一个指定类型的集合,像bool/int32/float/double/string这些类型都可以直接使用在proto协议中的某个message里,用作指定数据类型。
  • 而且一个message可以直接嵌套另一个message使用,被嵌套的message就相当于string,被认为是一种数据类型。

3. message中的编号和字段

  • message里标记1/2/3的数字,作用是标识每种类型元素在当前二进制编码中的专属tag,不可以重复
  • 编码的时候1-15的tag数字标签为一个bytes,16及以上的tag为两个bytes,所以设计message的时候尽量优化当前message数据结构,尽量给常用的或者使用过程中repeated比较多的数据使用1-15的tag数字,后续编码转换过程中可以保持最优状态
  • 每个message的字段必须要声明是required|repeated|optional,需要关注的是,一旦定义字段为required,后面就不能再修改,否则可能引起很多问题:
    • 新编辑的proto内容,不可以修改已存在的filed的tag值
    • 不能添加和删除任何required的filed
    • 新添加的filed要使用新的tag
    • 遵循以上原则才可以做到新旧版本message相互解析
  • required字段:一定要提供一个值给该字段,否则这条message会被认为“没有初始化”。序列化没有初始化的message会出现异常。解析一条没有初始化的message会失败。之外,这个required字段的行为更类似一个optional字段。
  • optional字段:该字段可以设置也可以不设置。如果一个可选字段没有设置值,会用缺省的值。系统对缺省的值定义是为0给整数类型,空串给字符串类型,False给布尔类型。optional类型的filed最好在tag编号后面和;前面给一个默认值[default = value],当然可以不指定,默认为空。
  • repeated字段:该字段会重复几次一些号码(包括0)。重复的值按顺序保存在protocl buffer中。重复的字段会被认为是动态的数组。

PS:required is forever,你应该非常小心地字段标记为required。如果在某一刻你希望停止写或发送一个必填字段,那就把不确定的字段更改为一个可选的字段---旧的阅读器会认为没有这个字段message是不完整的,而且可能会无意中拒绝或删除它们。你应该考虑为你的buffer编写特定于应用程序的自定义验证流程。某些来自Google的结论:使用required弊大于利;他们更愿意只用optional和repeated。

4. message方法

message类含有一些检查或操作整个message的方式,如下:

  • Isinitialized():检查是否所有required域都已赋值。
  • str():返回message的可读形式,可以通过str(message)或者print message触发,用于调试代码。
  • Clear():将所有域的赋值清空。
  • MergeFrom(other_msg):将给定的other_msg的内容合并到当前message,独立的域使用other_msg的值覆盖写入,repeated域的内容append到当前message的对应字段。独立的子message和group被递归的合并。
  • CopyFrom(other_msg):先对于message调用Clear()方法,再调用MergeFrom(other_msg)。
  • MergeFromString(serialized):将PB二进制字符解析后合并到本message,合并规则与MergeFrom方法一致。
  • ListFields():以(google.protobuf.descriptor.FieldDescriptor,value)的列表形式返回非空的域,独立的域如果HasField返回True则是非空的,repeated域至少包含一个元素则是非空的。
  • ClearField(field_name):清空某个域,如果被清空的域名不存在,抛出ValueError异常。
  • ByteSize():返回message占用的空间大小。
  • WichOneof(oneof_group):返回oneof组中被设置的域的名字或None,如果提供的oneof的组名不存在,抛出ValueError异常。

5. Enums

Enums是由metaclass扩展成一组具有符号常量的整数值。

6. 序列化和解析

每个message类都有序列化和解析方法:

  • SerializeToString():将message序列化并返回str类型的结果。(str类型只是二进制数据的一个容器而已,不是文本内容)。如果message没有初始化,跑出message.EncodeError异常。
  • SerializePartialToString():将message序列化并返回str类型的结果,但不检查message是否初始化。
  • ParseFromString(data):从给定的二进制str解析得到message对象。

PS:如果要在生成的PB类的基础上增加新功能,应采用包装(wrapper)的方式,永远不要将PB类作为基类派生子类添加新功能。

三、安装

# ===Linux 系统===

# 1.进入官网,下载对应系统的压缩包
# 2.解压包到自定义目录:
tar -zxvf protobuf-3.12.0.tar.gz

# 3.进入protobuf文件夹,进行配置,并make,命令如下:
cd protobuf-3.12.0
./configure
make && make install

# 4.make成功后再进行安装,进入刚解压的protobuf下的python目录进行安装(若安装VirtualEnv注意切换Python环境)
cd python
python setup.py build
python setup.py install
python setup.py test

# 5.完成
# ===Windows 系统===
'''注意:系统上要先装好python
windows系统需下载两个包,分别为:
(1) potoc-3.12.0-win64.zip(包含编译工具protoc.exe,protoc.exe需放在C:\protobuf-python-3.12.0\protobuf-3.12.0\src 和 C:\Windows\System32下,据说这样能包治百bug)
(2) protobuf-python-3.12.0.zip(包含protobuf与语言python之间的protobuf运行时的库,转换时需要,相当于protobuf与各语言之间的协定格式。解压放在C盘根目录下)
'''

# 1.进入官网,下载对应系统的两个压缩包(参考上面的说明)
# 2.解压,根据路径配置好文件
protobuf-python-3.12.0.zip 解压---> C:\
potoc-3.12.0-win64.zip 解压---> 任意
复制potoc-3.12.0-win64.zip解压出来的protoc.exe 到 C:\protobuf-python-3.12.0\protobuf-3.12.0\src 和 C:\Windows\System32下
        
# 3.打开cmd开始安装,cmd切换到C:\protobuf-python-3.12.0\protobuf-3.12.0\Python目录下,依次执行下列命令
python setup.py build
python setup.py test
python setup.py install

# 4.完成

四、使用

接下来我们通过一个大栗子来演示一遍上面枯燥的文字。还是文章开头的模板。

1. 创建一个addressbook.proto文件

// proto2的语法声明,可加可不加,目前已到proto3版本
syntax = "proto2";

// 包名称
package tutorial;

// 定义message字段类型
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;
}

2. 编译addressbook.proto

写好.proto文件后下来就可以进行编译,以便在python上使用。

Windows是cmd命令行,linux和mac直接用命令行输入,编译完成后会生成一个叫addressbook.bp2.py的文件。
命令如下:

protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
# -I :大写的I是input,意为需要编译的.proto文件路径,可以是当前路径(. 或 ./)或绝对路径
# --python_out:编译生成的xxx.bp2.py文件的保存路径。
# $SRC_DIR/addressbook.proto :最后写需要用源目标的哪个proto文件进行编译。

addressbook.bp2.py文件内容比较多,我们就不展示了,可以打开进去看看。

无论生成的是java或是C++的protocol buffer代码,python protocol buffer编译器都不会生成可以直接访问数据的代码。就是你看到的addressbook_pb2.py里的内容那样。它会为你的message、枚举、字段生成指定的描述符,还有一些不好理解的空类,其中一段为:

class Person(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType

class PhoneNumber(message.Message):
    __metaclass__ = reflection.GeneratedProtocolMessageType
    DESCRIPTOR = _PERSON_PHONENUMBER
  DESCRIPTOR = _PERSON

class AddressBook(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType
  DESCRIPTOR = _ADDRESSBOOK

每个类中都有一些重要的语句:__metaclass__ = reflection.GeneratedProtocolMessageType。可以看做是创建类的模板。加载时,GeneratedProtocolMessageType metaclass会用python的方式指定描述符创建时需要用的message类型和添加和这些方法相关的类。然后就可以在代码中使用这些类。

3. 调用 addressbook.bp2.py文件

3.1 Pycharm的使用方法之一:

把addressbook.bp2.py文件直接复制或鼠标拖到pycharm的当下project目录下,然后鼠标放在pycharm当前project目录上右键依次:make_directory as ---> sources path ,将当前工作的文件夹加入source_path,就可以使用了。

3.2 数据的序列化与解析

  1. 调用Protocol buffer序列化数据
#!/usr/bin/env python3
# coding:utf-8

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 = "GTWANG@gamil.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())

注意:通过protocol buffer生成的是二进制文档,所以上面例子最后写入文件的时候要用bytes类型。

2)读取和解析已序列化的数据

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)

五、结束语

一个简单的序列化数据就完成了,仅需制作一次自定义的格式化的数据,就可以使用Protbuf编译器生成各种编程语言的源码,而且更小、更快、更简单。

这只是一个Python版本的简单的栗子,还有更多功能等着你发现哦。