如何在 Python 中实现 Protobuf

2025年3月17日 | 阅读 8 分钟

在本教程中,我们将学习 Google 的 Protobuf 以及如何在 Python 编程语言中实现它。

假设有一群来自不同地方、讲不同语言的人。为了有效地沟通,他们尝试使用一种所有人都懂的语言。因此,小组中的每个人都会尝试将他们的想法翻译成他们决定使用的语言。这种沟通方式将成功地向整个小组传达信息。但是,这会导致效率、速度和准确性的损失。

计算机系统及其组件也会出现同样的情况。当我们将信息从一端交换到另一端时,我们需要将数据转换为 XML、JSON 或任何人类可读的格式,这需要花费大量时间。为什么我们不以一种最大限度地减少数据丢失并提高速度的方式来编码数据呢?

Google 以 Protobuf 的形式提出了解决方案。我们将使用 Protobuf 并了解它与常规数据编码有何不同。让我们从 Protobuf 的介绍开始。

Protobuf 简介

Protobuf 由科技巨头 Google 开发,它是一种数据传输方式。它有效地缩小了数据块,从而提高了数据发送速度。它是一种以快速高效的方式将数据序列化为二进制流的技术。它被设计为平台中立的格式,并将数据抽象为一种语言。它用于机器间通信RPC (远程过程调用)。

要开始使用 Protobuf,我们应该至少了解一种编程语言来描述数据的形状。然而,Protobuf 最好的地方在于它支持许多编程语言,例如 Python、Java、C++、JavaScript、C 等。

什么是协议缓冲区及其工作原理?

协议缓冲区是用于结构化数据序列化的定义接口。它建立了与数据的通信。它与平台和语言无关。根据 Google 官方定义 -

“协议缓冲区是 Google 的语言中立、平台中立、可扩展的结构化数据序列化机制——想象一下 XML,但更小、更快、更简单。你定义一次你想要数据如何结构化……”

所以问题是,当有其他方法可以对数据进行编码,例如 pickling、JSON 和 XML 时,我们为什么需要协议缓冲区。

  • Python 提供了 pickling,这是一种内置的数据序列化方法,但它不能很好地处理模式演变。当我们需要在跨语言应用程序(用 Java 或 C++ 编写)之间共享数据时,它也无法正常工作。
  • 将数据序列化为 XML - XML 方式变得非常有吸引力,因为它有点人类可读,并且有许多语言的绑定库。它也适用于跨语言应用程序之间的数据交换。但是,XML 可能会占用大量的空间,并且在编码/解码时,它可能会导致应用程序出现巨大的速度问题。

协议缓冲区克服了所有这些限制。在协议缓冲区中,我们创建包含我们想要存储的数据结构的 .proto 文件。创建 proto 文件后,protobuf 编译器就会发挥作用。protobuf 编译器生成一个类,该类使用高效的二进制格式实现数据的自动编码和解析。我们需要通过命令指定语言来创建类。生成的类允许我们根据 .proto 文件实例化新消息来创建元素。我们将在下一节中详细了解所有这些过程。

Python Protobuf 缓冲区

Protobuf 是为跨语言应用程序之间的数据共享而开发的。让我们看一个如何创建 protobuf 的例子。首先,我们需要在 .proto 文件中指定我们的数据结构。让我们看以下 student.proto 文件的例子。

正如我们所看到的,语法与 C++ 或 Java 非常相似。让我们了解上面文件的结构。

  • 每个 proto 文件都以 proto_version 开头,它可以是 proto2 或 proto3。我们使用 proto3,因为它支持所有主要版本的编程语言。
  • 第二行是包声明,它减少了不同项目之间命名冲突的可能性。
  • 接下来我们定义了消息定义。它只是一个聚合,用于保存消息的字段。有许多标准类型字段可用,如 bool、string、double、int32、float 和 double。

在创建 proto 文件之前,我们需要安装名为 protoc 的 proto 编译器。要安装它,请访问 Github 并下载 python protoc 编译器。我们安装了 64 位版本的 Windows。

How to Implement Protobuf in Python

它将在系统中下载 protoc 编译器。

注意 - 要在任何地方运行 protoc 编译器,最好将其路径添加到系统环境变量中。您可以将其 C:\Users\User\Desktop\protoc-3.19.4-win64\bin 添加到系统路径。

让我们在 student.proto 文件中添加一些数据。

让我们了解上面文件的详细结构。

在第一行中,我们将 proto 版本定义为 proto3,第二行包含包名。

正如您所观察到的,我们在每个元素上都定义了 =1 或 =2 或 =3 标记,这些标记用于标识字段在二进制编码中使用的唯一“标签”。我们应该记住,值 1-15 比更高的数字编码时少一个字节。所以我们可以将常用或重复的元素用于 1-15,将更高的数字用于不常用或可选的元素。

我们定义了 Enum 类型,它是一个枚举,用于列出给定变量的可能值。

我们需要附加以下修饰符之一。

  • 可选 (optional) - 这意味着我们不需要传递值。如果我们不提供值,它将使用默认值。默认值可以像我们在结果字段中那样自己定义。
  • 重复 (repeated) - 它类似于动态大小的数组。带有 repeated 注释的字段可以重复零次或任意次数。有序或重复的值将保留在协议缓冲区中。
  • 必需 (required) - 此类字段必须获取某个值,否则该字段将未初始化并在序列化时引发错误。

编译协议缓冲区

现在我们将使用协议缓冲区编译器 (protoc) 生成集成代码到特定语言的集成中。我们使用以下命令。

在上面的命令中,有三个参数。

  • -I:- 它定义了我们的 proto 文件所在的源目录。我们可以使用点 (.) 表示当前目录。
  • --python-out - 它指定了生成代码的语言。我们可以使用任何受支持的语言。我们正在使用 Python,所以使用 --python-out。
  • $SRC_DIR 和 $DST_DIR - $SRC 表示源目录,$DST 表示目标目录。

我们运行 protoc -I=. --python_out=. student.proto 命令,它生成了相应的代码。

注意 - 如果您遇到 google_protobuf 模块未找到错误,只需使用以下命令安装 google_protobuf 库。

student_pb2.py 文件

文件的结构看起来相当混乱,我们无法立即理解。

最有趣的部分是使用生成的代码创建、构建和序列化数据。让我们看看创建序列化数据的以下集成部分。

创建新文件并复制以下代码。

输出

student_id: 101
student_name: "Megha"
student {
  total_marks: 500
  marks_obtain: 425
}

student_id: 102
student_name: "Peter"
student {
  result: FAIL
  total_marks: 500
  marks_obtain: 136
}

我们创建学生列表并逐个添加元素。然后,我们打印学生列表元素。它返回非二进制、非序列化版本。我们可以使用 SerializeToString()ParseFromString() 将消息编码为二进制格式。

在上面的代码中,我们已经序列化了字节数据,并使用 wb 标志写入文件。

我们可以读取这个二进制文件并使用 ParseFromString() 方法解析它。它将返回字符串的字节表示。它看起来像下面这样。

引号前的 b 表示 Python 中由字节八位字节组成的以下字符串。数据的 XML 表示如下。

JSON 版本如下。

{
student_id: 101
student_name: "Megha" {
student {
  total_marks: 500
  marks_obtain: 425
}
}
}

当我们观察这些不同格式占用的内存时,我们可以很容易地发现二进制部分使用的内存比任何文件都少。在我们的例子中,它只占用 17 字节,与其他格式相比相当少。想想传输成千上万的消息,这在内存方面会产生巨大的差异。

注意 - 我们已将结果值的默认值定义为 FAIL。如果未分配或更改属性,它们将使用默认值。在我们的例子中,如果我们不修改 StudentList 的 StudentResult。未设置的值不会被序列化,从而节省了额外的内存空间。如果我们将它从 PASS 更改为 FAIL,它将如下所示。

student_id: 101
student_name: "Mathew"
student {
  total_marks: 500
  marks_obtain: 136
}

结论

协议缓冲区在数据传输的速度和效率方面起着至关重要的作用。可能需要一些时间才能熟练使用它;然而,定义新消息非常简单。在本教程中,我们介绍了如何使用 Python 语言实现 Protobuf 并序列化数据。本教程还包括一个简单的应用程序创建,但它可以超越简单的访问器和序列化。