Protobuf学习入门(一)

  • 时间:
  • 来源:互联网
  • 文章标签:

  笔者最近在学习使用tensorflow/serving,其中有不少涉及Protobuf相关的内容,因此接触学习了Prorobuf,记录于此,希望能对读者有所启发。

  本文作为Protobuf入门学习的第一篇文章,将简单介绍Protobuf协议以及如何使用Protobuf来实现序列化与反序列化。

Protobuf简介

protobuf

  Protobuf即Protocol Buffers,是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议。

  与XML和JSON格式相比,Protobuf更小、更快、更便捷。Protobuf是跨语言的,并且自带一个编译器(protoc),只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。

  只需要将要被序列化的结构化数据定义一次(在.proto文件定义),便可以使用特别生成的源代码(使用Protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新.proto文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。

  Protobuf的优点如下:

  • 性能号,效率高。序列化后字节占用空间比XML少3-10倍,序列化的时间效率比XML快20-100倍。
  • 有代码生成机制。将对结构化数据的操作封装成一个类,便于使用。
  • 支持向后和向前兼容。当客户端和服务器同时使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用。
  • 支持多种编程语言。Protobuf目前已经支持Java,C++,Python、Go、Ruby等多种语言。

  Protobuf的缺点如下:

  • 二进制格式导致可读性差
  • 缺乏自描述
  • 应用不是很广泛

  现阶段Protobuf的应用场景主要有Google的gRPC, Tensorflow/Serving等。

Protobuf安装

  这里介绍在Linux环境中如何安装Protobuf编译器,以及对应的Python模块。

  首先,下载Protobuf的源代码:

wget https://github.com/google/protobuf/releases/download/v3.6.0/protobuf-python-3.6.0.tar.gz

  解压文件,编译并安装:

tar zxvf protobuf-python-3.6.0.tar.gz
cd protobuf-3.6.0
./configure
make
make check
make install

  验证Protobuf是否安装成功:

protoc --version
libprotoc 3.6.0(笔者电脑上的输出结果)

  Protobuf对应的Python第三方模块为protobuf,直接使用pip安装即可:pip3 install protobuf,验证安装成功的办法如下:

$ python
Python 3.6.10 |Anaconda, Inc.| (default, May  8 2020, 02:54:21) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.protobuf
>>>

如果导入第三方模块没有报错,则表明该模块安装成功。

.proto文件格式说明

  在前面的简介部分已经说过,Protobuf在使用时定义序列化结构的文件为后缀是.proto的文件。

  .proto文件有专门的语法结构,ProtoBuf有两个语法版本:v2与v3。message 用来定义一个数据结构。

  我们先来看一个简单的.proto文件的例子:

syntax = "proto3";

message Person {
    int64 id = 1;
    string name = 2;
    repeated string skills = 3;  // 这里表示skills可以接受多个string类型的值
}

文件的首行生命该语法使用Protobuf3语法,同时在文件后面定义了Person消息,该消息有三个字段:id, name, skill。

  每个字段的定义格式为 指定字段规则 数据类型 变量名称=数字标识符

  指定字段规则在Protobuf3语法中只有repeated、singular两种类型,其中singular类型(默认类型,不需要声明)表示有0个或者1个这种字段(但是不能超过1个);repeated类型表示该字段可以重复任意多次(包括0次),重复值的顺序会被保留。

  数据类型常见的有double、float、int32、string、bytes、bool等,也可以是枚举、嵌套消息类型、Any、oneof等。

  在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。最小的标识号可以从1开始,最大到2^29-1(536,870,911)。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。

注:[1,15]之内的标识号在编码的时候会占用一个字节。
[16,2047]之内的标识号则占用2个字节。
所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。
切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

序列化与反序列化

  下面将通过一个简单的里来介绍如何使用Protobuf来实现序列化与反序列化。

  定义数据结构文件(person_and_book.proto)如下:

syntax = "proto3";

message Book {  // 书籍信息
  string name = 1;
  float price = 2;
  string press = 3;
  repeated Person people = 4;
}

message Person {  // 人物信息
  int32 id = 1;
  string name = 2;
  int32 age = 3;
  string email = 4;
  string job = 5;
  bool work_status = 6;
  string city = 7;

  MyAddress maps = 8;
}

message MyAddress { // 地址信息,字段类型为map
  map<string, string> tell_address = 1;
}

  使用protoc编译person_and_book.proto文件, 命令行如下:

protoc ./person_and_book.proto  --python_out=./

编译完毕,会自动生成person_and_book_pb2.py文件。在命令行中,./person_and_book.proto为需要编译的.proto文件所在路径,python_out为输出python脚本路径,./表示为当前路径。

  接着我们使用一个新的脚本(add_person.py)针对该数据结构进行序列化与反序列化。完整代码如下:

# -*- coding: utf-8 -*-
import person_and_book_pb2

# 书籍信息
book = person_and_book_pb2.Book()
book.name = "Introduction to Protobuf"
book.price = 8.5
book.press = "NY Press"

# 添加人物信息
person = book.people.add()
person.id = 1
person.name = "protobuf"
person.age = 25
person.email = "protobuf@163.com"
person.job = "college professor"
person.work_status = True
person.city = "Shanghai"

# 添加人物的地址信息
address_maps = person.maps
address_maps.tell_address["born place"] = "SX"
address_maps.tell_address["living place"] = "SH"
address_maps.tell_address["visited place"] = "BJ, GZ, SY"

# 序列化
serializeToString = book.SerializeToString()
print(type(serializeToString), serializeToString)

# 反序列化
parsed_book = person_and_book_pb2.Book()
parsed_book.ParseFromString(serializeToString)
print(type(parsed_book))

# 输出书籍信息
print("book_name: %s, book_price: %s, book_press: %s" % (parsed_book.name, parsed_book.price, parsed_book.press))

# 输出人物信息
for person in parsed_book.people:
    print("p_id: %s, p_name: %s, p_age: %s, p_email: %s, p_job: %s, p_work_status: %s, p_city: %s"
          % (person.id, person.name, person.age, person.email, person.job, person.work_status, person.city))

    for key in person.maps.tell_address:
        print(key, person.maps.tell_address[key])

输出结果如下:

<class 'bytes'> b'\n\x18Introduction to Protobuf\x15\x00\x00\x08A\x1a\x08NY Press"\x84\x01\x08\x01\x12\x08protobuf\x18\x19"\x10protobuf@163.com*\x11college professor0\x01:\x08ShanghaiBC\n\x1b\n\rvisited place\x12\nBJ, GZ, SY\n\x12\n\x0cliving place\x12\x02SH\n\x10\n\nborn place\x12\x02SX'
<class 'person_and_book_pb2.Book'>
book_name: Introduction to Protobuf, book_price: 8.5, book_press: NY Press
p_id: 1, p_name: protobuf, p_age: 25, p_email: protobuf@163.com, p_job: college professor, p_work_status: True, p_city: Shanghai
born place SX
living place SH
visited place BJ, GZ, SY

从文件中读取message

  google.protobuf模块中的text_format脚本允许我们直接对文本格式的消息进行解析。

  我们有如下文本格式的消息(book_example.ini):

name: "Learning Protobuf",
price: 10,
press: "SH Edu Press",
people {
  id: 2
  name: "JC",
  age: 28,
  email: "jc@qq.com",
  job: "IT",
  work_status: True,
  city: "SH",
  maps {
      tell_address: {key: "born place", value: "SX"},
      tell_address: {key: "living place", value: "HZ"},
      tell_address: {key: "visited place", value: "LZ, HZ, SZ, YZ, DH, JYG"},
      tell_address: {key: "educated place", value: "SX, HZ, SH"},
  }
}

  解析该文本格式消息的代码(read_message_from_text)如下:

# -*- coding: utf-8 -*-
from google.protobuf import text_format
import person_and_book_pb2

# message: Book
book = person_and_book_pb2.Book()

model_config_file_path = "book_example.ini"
with open(model_config_file_path, 'r+') as f:
    book_ini = f.read()

parsed_book = text_format.Parse(text=book_ini, message=book)

# 输出解析信息
# 输出书籍信息
print("book_name: %s, book_price: %s, book_press: %s" % (parsed_book.name, parsed_book.price, parsed_book.press))

# 输出人物信息
for person in parsed_book.people:
    print("p_id: %s, p_name: %s, p_age: %s, p_email: %s, p_job: %s, p_work_status: %s, p_city: %s"
          % (person.id, person.name, person.age, person.email, person.job, person.work_status, person.city))

    for key in person.maps.tell_address:
        print("{}: {}".format(key, person.maps.tell_address[key]))

输出结果如下:

book_name: Learning Protobuf, book_price: 10.0, book_press: SH Edu Press
p_id: 2, p_name: JC, p_age: 28, p_email: jc@qq.com, p_job: IT, p_work_status: True, p_city: SH
visited place: LZ, HZ, SZ, YZ, DH, JYG
born place: SX
living place: HZ
educated place: SX, HZ, SH

oneof类型

  oneof类型用来代表在实现的时候,该组属性中有且只能有一个被定义,不能出现多个。

  我们有如下的数据结构(oneof_test.proto):

syntax = "proto3";

message Test {
    oneof message {
        MessageA a = 1;
        MessageB b = 2;
    }
}

message MessageA {
    string content = 1;
}
message MessageB {
    int32 content = 1;
}

  编译生成oneof_test_pb2.py。现在需要将一个Test消息的a字段设置为MessageA,另一个Test消息的b字段设置为MessageB,代码(set_onof_field)如下:

# -*- coding: utf-8 -*-
from oneof_test_pb2 import Test, MessageA, MessageB

message_a = MessageA()
message_a.content = "Hello from MessagesA!"

message_b = MessageB()
message_b.content = 1

# 设置test.a为MessageA
test1 = Test()
test1.a.CopyFrom(message_a)
# 设置test.a为MessageB
test2 = Test()
test2.b.CopyFrom(message_b)

print(test1)
print(test2)

输出结果如下:

a {
  content: "Hello from MessagesA!"
}

b {
  content: 1
}

总结

  本文演示的例子都已放至Github,访问地址为:https://github.com/percent4/protobuf_learning 。

  本文到此结束,感谢大家的阅读~

参考网址

  1. python基础–protobuf的使用(一): https://blog.csdn.net/u013210620/article/details/81317731
  2. Github/protobuf: https://github.com/protocolbuffers/protobuf
  3. gRPC快速入门(一)——Protobuf简介: https://blog.51cto.com/9291927/2331980?source=drh
  4. Protobuf3语法详解: https://blog.csdn.net/qq_36373500/article/details/86551886
  5. protobuf文本格式解析地图: http://www.voidcn.com/article/p-yrhvscxz-bwp.html
  6. Question: How to set oneof fields in python?: https://github.com/protocolbuffers/protobuf/issues/5012

本文链接http://www.taodudu.cc/news/show-1781824.html