ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从 .proto 到 Python: 使用 Protocol Buffers 的完整实践指南

从 .proto 到 Python: 使用 Protocol Buffers 的完整实践指南

✨ 为什么使用 Protobuf?

Protocol Buffers(简称 protobuf)是 Google 推出的高效、跨语言、结构化数据序列化协议。相比 JSON / XML,它具备:

  • 📦 结构清晰:强类型 + 字段编号
  • 🚀 更高效:体积小,速度快
  • 🔁 跨语言:支持 Python / Java / Go / C++ 等
  • 🔐 序列化 & 反序列化自动完成

🧱 使用步骤概览

使用 protobuf 的标准流程如下:

1. 定义数据结构(.proto 文件)
2. 使用 protoc 编译生成 Python 文件
3. 在业务代码中导入并使用

✍️ Step 1:定义 .proto 文件

1. 语法说明:

syntax = "proto3";              // ① 语法版本(必须)
package recsys.user;            // ② 命名空间(推荐使用)import "common.proto";          // ③ 引入其他 proto(可选)message UserProfile {           // ④ 定义结构体(message)string user_id = 1;           // ⑤ 字段:类型 + 名字 + 标签号int32 age = 2;string gender = 3;
}

2. 工程中最常用的 Protobuf 结构类型

类型用途场景举例
message定义对象结构User, Product, Item
enum定义类型/状态用户类型、策略类型
oneof互斥字段节省空间多设备 ID,登录方式
map表达动态字段用户兴趣、召回特征
import模块拆分多人协作、复用字段结构
1)message —— 定义结构体对象(核心)
message User {string user_id = 1;int32 age = 2;string gender = 3;
}
  • ✅ 用途:表示一个完整的数据对象(用户、视频、行为、商品等)
  • ✅ 工程中最核心的数据建模单位
2) enum —— 表示固定值集合(类型、安全)
enum ProductType {UNKNOWN = 0;PHONE = 1;PLAN = 2;BROADBAND = 3;
}
  • ✅ 用途:限定字段只能取固定几个值,类型更安全
  • ✅ 工程中常用于 状态码、产品类型、行为类型
3) oneof —— 表示互斥字段(节省空间)
message DeviceId {oneof device {string imei = 1;string idfa = 2;string mac = 3;}
}
  • ✅ 用途:多个字段只能选一个
  • ✅ 工程中常见于“设备标识”、“登录方式”
4) map<key, value> —— 键值对结构(灵活字段)
message UserProfile {map<string, float> interest_score = 1;
}
  • ✅ 用途:表达稀疏结构,如“用户标签-分数”、“ID-特征值”
  • ✅ 常见于推荐特征、画像存储
5) import —— 模块化复用结构
import "common.proto";message FeedItem {common.Device device = 1;string item_id = 2;
}
  • ✅ 用途:支持拆分多个 proto 文件,团队协作管理
  • ✅ 工程中常见:公共字段/结构统一定义后 import

3 最常用组合示例(高频出现)

syntax = "proto3";
package recsys;import "common.proto";enum EventType {UNKNOWN = 0;CLICK = 1;VIEW = 2;PURCHASE = 3;
}message UserEvent {string user_id = 1;EventType type = 2;map<string, float> features = 3;common.DeviceId device = 4;
}

🛠️ Step 2:使用 protoc 编译生成 Python 文件

基本命令格式:

protoc --proto_path=proto_dir --python_out=out_dir proto_file
各参数含义详解:
参数示例说明
protoc(命令)Protobuf 的编译器命令,必须先安装
--proto_path=PROTO_DIR
或 -I=PROTO_DIR
--proto_path=proto/指定 .proto 文件所在目录;支持多个路径;用于支持 import
--python_out=OUT_DIR--python_out=gen/指定生成的 Python 文件保存的目标目录
PROTO_FILEproto/history.proto要编译的 .proto 文件路径,可以是相对路径

实际示例:

假设你有以下结构:

project/
├── proto/
│   └── history.proto
└── gen/

执行编译:

protoc --proto_path=proto --python_out=gen proto/history.proto

就会生成文件:gen/history_pb2.py

🧪 Step 3:在 Python 中使用

✅ Python 示例代码

# 导入 message 类
from gen.history_pb2 import Video# 构造一个 Video 实例
video = Video(video_id="abc123",video_time=120.0,watch_time=90.0,event_time=1720001234
)# 打印结构(等价于 JSON)
print("video =", video)# ✅ 序列化:转为二进制(可存入 Redis、Kafka、文件)
binary_data = video.SerializeToString()
print("binary_data =", binary_data)# ✅ 反序列化:从二进制还原成对象
video_parsed = Video()
video_parsed.ParseFromString(binary_data)print("parsed =", video_parsed.video_id, video_parsed.watch_time)## ✅ 转 JSON 也很方便(调试/对外)
from google.protobuf import json_formatvideo_json = json_format.MessageToJson(video)
print(video_json)# JSON → 对象
video2 = Video()
json_format.Parse(video_json, video2)

🔍 常用方法速查表

方法用途
SerializeToString()对象 → 二进制(发送、存储)
ParseFromString(data)二进制 → 对象(读取、解析)
CopyFrom(obj)拷贝另一个对象的数据
Clear()清空所有字段
HasField("field_name")判断某字段是否被设置(仅限 message/oneof)
video.field_name访问字段
video.ListFields()获取已赋值字段及值(调试用)

🌁 一个完整的例子

  1. 创建 history.proto 文件,内容如下:
syntax = "proto3";
package history;message Video {string video_id = 1;float video_time = 2;float watch_time = 3;int32 event_time = 4;
}message VideoHistory {repeated Video videos = 1;
}
  • video_id:视频 ID
  • video_time:视频总时长
  • watch_time:观看时长
  • event_time:发生时间(秒级时间戳)
  • repeated:表示数组/列表
  1. 执行:
protoc --proto_path=. --python_out=./dto history.proto

执行后,自动生成 dto/history_pb2.py 文件,不要手动编辑这个文件,它是自动生成的。

  1. python 调用
from dto.history_pb2 import Video, VideoHistory# 构造单个视频数据
video = Video(video_id="vid_001",video_time=120.0,watch_time=90.0,event_time=1720001234
)# 构造历史记录
history = VideoHistory()
history.videos.append(video)# 序列化为二进制(可用于网络传输、存储)
binary_data = history.SerializeToString()# 反序列化回来
new_history = VideoHistory()
new_history.ParseFromString(binary_data)print("恢复的视频ID:", new_history.videos[0].video_id)
📦 Protobuf vs JSON 在 Redis 中的使用对比
对比项Protobuf(二进制)JSON(字符串)
体积✅ 小❌ 大
性能✅ 快(原生序列化)❌ 慢(字符串解析)
可读性❌ 差✅ 强
强类型结构✅ 有❌ 无
版本兼容性✅ 好❌ 差(需靠约定)
调试难度❌ 高✅ 低
🔁 Protobuf + Redis 封装
def append_video(redis, uid: str, video: Video, max_len=50):key = f"user_click:{uid}"redis.lpush(key, video.SerializeToString())redis.ltrim(key, 0, max_len - 1)def get_user_clicks(redis, uid: str, limit=50):key = f"user_click:{uid}"raw_list = redis.lrange(key, 0, limit - 1)return [deserialize_video(raw) for raw in raw_list]def deserialize_video(raw: bytes) -> Video:v = Video()v.ParseFromString(raw)return v
返回列表