✨ 为什么使用 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_FILE | proto/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() | 获取已赋值字段及值(调试用) |
🌁 一个完整的例子
- 创建
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:视频 IDvideo_time:视频总时长watch_time:观看时长event_time:发生时间(秒级时间戳)repeated:表示数组/列表
- 执行:
protoc --proto_path=. --python_out=./dto history.proto
执行后,自动生成 dto/history_pb2.py 文件,不要手动编辑这个文件,它是自动生成的。
- 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