☰
protobuf解析xls工具:从Excel配置表到二进制数据的自动化链路
2026/10/20 12:26:54 网站建设 项目流程

简介:这是一款面向Unity游戏开发者的数据转换工具,通过protobuf序列化技术将Excel表格解析并生成可直接使用的C#类文件,解决游戏开发中数据管理与频繁更新的痛点。使用者需具备Python、protobuf-net及Unity基础,适合需要以表格驱动角色属性、物品配置等数据的项目。资源包共34个文件,约375KB,以cpp与h源码为主,辅以SConstruct构建脚本、Python转换脚本及示例xls文件,另含README与TODO文档说明用法,整体结构紧凑。目前已有663人学习下载。工具核心价值在于打通xls到Unity资产的工作流,开发者只需修改表格即可调整游戏数据,无需改动源代码,显著提升迭代效率;同时可借此理解protobuf在.NET环境下的集成方式与序列化机制,为跨平台数据通信提供参考。

1. 从一份“祖传”配置表说起:protobuf解析xls工具到底解决什么问题

如果你接过那种用 Excel 当数据库用的项目,大概率见过这种场面:策划丢过来一个item.xls,里面几百行道具配置,程序这边要把它转成.pb二进制给服务器和客户端同时用。手动导?改一次错一次。写脚本?每个项目都要重写一遍。这个 protobuf 解析 xls 工具就是冲着这个场景来的——它把 xls 表格按约定规则解析出来,直接生成 protobuf 可用的数据文件和对应的结构定义,让配置表到二进制这条链路自动化。

它适合两类人:一类是游戏或后端项目里需要频繁处理 Excel 配置表的开发者,另一类是正在学 protobuf 序列化、想找个真实小工具拆开看实现的人。工具本身不复杂,但麻雀虽小,从表格解析、类型映射到 protobuf 编码,整条链路都齐了,拿来当二次开发的底子很合适。

2. 技术选型与整体架构:为什么是 xls + protobuf 这套组合

2.1 选型逻辑:表格做输入、protobuf 做输出

先说为什么输入是 xls 而不是 csv 或 json。csv 没有多 sheet、没有类型标注、没有合并单元格,策划用起来骂声一片;json 倒是灵活,但手写容易漏字段、多逗号,而且体积大。xls 是策划最顺手的工具,能加批注、能标颜色、能分 sheet,所以工具选择直接吃 xls,把“人写的那部分”留在 Excel 里。

输出选 protobuf 而不是自定义二进制,核心原因是跨语言。服务器可能是 C++ 或 Go,客户端可能是 C# 或 Java,protobuf 生成的代码各语言都有,字段增删还能保持向后兼容。常见做法是:工具读 xls,生成.proto文件描述结构,再生成.bytes或.pb数据文件,业务侧用对应语言的 protobuf 库反序列化即可。

2.2 整体架构:三层结构

这个工具在结构上可以拆成三层,理解这三层,后面看代码就不迷路:

层级职责关键输入/输出
表格解析层读取 xls,识别表头、类型行、数据行输入 .xls,输出内存中的行列表
类型映射层把表格里的类型字符串映射成 protobuf 类型输入 “int32/string/array”,输出 FieldDescriptor
代码与数据生成层生成 .proto 定义和序列化后的数据文件输出 .proto + .bytes

三层之间是单向依赖:解析层不认识 protobuf,只负责把表格读成结构化数据;映射层做翻译;生成层才碰 protobuf 库。这样拆的好处是,哪天你想把输出从 protobuf 换成 flatbuffers,只需要动第三层。

2.3 目录结构长什么样

一个典型的工程目录大概是这样组织的(不同实现会有差异,但思路一致):

proto-xls-tool/ ├── main.py # 入口,命令行参数解析 ├── parser/ │ ├── xls_reader.py # xls 读取与表头解析 │ └── type_mapper.py # 类型映射 ├── generator/ │ ├── proto_writer.py # 生成 .proto │ └── data_writer.py # 生成序列化数据 ├── template/ │ └── message.tpl # proto 模板 └── config/ └── mapping.json # 自定义类型映射配置

config/mapping.json是个容易被忽略但很关键的文件,它让你不用改代码就能调整类型对应关系,比如把表格里的 “道具ID” 映射成int32,把 “描述” 映射成string。

3. 核心模块拆解:从读表到生成 .proto 的完整链路

3.1 读表:怎么把 xls 的表头和类型行认出来

约定是这样的:第一行是字段名(英文,给 protobuf 用),第二行是类型(int32、string、array 等),第三行是中文注释(可选),从第四行开始是真实数据。读表模块要做的就是按这个约定把每一列的信息抽出来。

import xlrd def read_sheet(xls_path, sheet_name): """读取指定 sheet,返回 (字段列表, 数据行列表)""" book = xlrd.open_workbook(xls_path) sheet = book.sheet_by_name(sheet_name) # 第一行字段名,第二行类型,第三行注释 field_names = sheet.row_values(0) field_types = sheet.row_values(1) field_comments = sheet.row_values(2) fields = [] for i in range(sheet.ncols): name = field_names[i].strip() if not name: # 空列直接跳过,策划经常留空列 continue fields.append({ "name": name, "type": field_types[i].strip(), "comment": field_comments[i].strip() if i < len(field_comments) else "" }) # 从第 4 行开始是数据 rows = [] for r in range(3, sheet.nrows): row_values = sheet.row_values(r) if not any(str(v).strip() for v in row_values): continue # 整行为空跳过 rows.append(row_values) return fields, rows

这段代码有几个细节值得说。sheet.ncols是按最大列数算的,如果策划在很靠右的地方误填了一个值,会多出空列,所以用if not name: continue过滤掉没有字段名的列。数据行判断any(...)是为了跳过 Excel 里常见的“看起来空但实际有格式”的行,这种行直接读会得到空字符串,不处理的话会生成一条全空记录。

参数上,xls_path是文件路径,sheet_name指定读哪个 sheet。如果你的表是多 sheet 的,外层再套一层循环遍历book.sheet_names()就行。

3.2 类型映射:把表格里的“int32”翻译成 protobuf 类型

表格里写的类型是给人看的,protobuf 要的是它自己那套标量类型。映射层做两件事:一是把简单类型对上,二是处理数组和嵌套。

# 基础类型映射表 TYPE_MAP = { "int": "int32", "int32": "int32", "int64": "int64", "float": "float", "double": "double", "string": "string", "bool": "bool", } def map_type(raw_type): """把表格类型字符串转成 protobuf 类型定义""" raw_type = raw_type.strip().lower() # 数组类型:array<int32> 或 int32[] if raw_type.startswith("array<") and raw_type.endswith(">"): inner = raw_type[6:-1] return "repeated " + TYPE_MAP.get(inner, "string") if raw_type.endswith("[]"): inner = raw_type[:-2] return "repeated " + TYPE_MAP.get(inner, "string") # 普通类型 return TYPE_MAP.get(raw_type, "string") # 认不出来就当 string,别让工具崩

这里有个血泪经验:类型映射一定要有兜底。策划写类型的时候手滑打出 “int3” 或者 “str”,如果工具直接抛异常,整个导出流程就断了。默认回退到string虽然不完美,但至少能跑完,导出后再看日志修正。repeated是 protobuf 里表示数组的关键字,对应表格里一列填多个值的情况,通常用逗号分隔。

3.3 生成 .proto:字段编号和语法版本怎么定

protobuf 每个字段都要有唯一编号,这个编号不能随便变,否则新旧数据对不上。常见做法是按列顺序从 1 开始递增,并且把编号规则固定下来,后续加字段只能往后加,不能插在中间。

def generate_proto(message_name, fields): """根据字段列表生成 proto3 定义""" lines = ['syntax = "proto3";', ''] lines.append(f"message {message_name} {{") for idx, field in enumerate(fields, start=1): pb_type = map_type(field["type"]) comment = f" // {field['comment']}" if field["comment"] else "" lines.append(f" {pb_type} {field['name']} = {idx};{comment}") lines.append("}") return "\n".join(lines)

生成出来的东西大概长这样:

syntax = "proto3"; message ItemConfig { int32 id = 1; // 道具ID string name = 2; // 道具名称 repeated int32 effect = 3; // 效果列表 }

字段编号从 1 开始,是因为 protobuf 里 1 到 15 的编号编码后只占一个字节,把最常用的字段放前面能省空间。syntax = "proto3"是现在的主流选择,proto2 的 required 语义在新项目里基本不用了。注释直接跟在字段后面,方便策划和程序对照。

3.4 生成数据文件:把每一行序列化成二进制

有了 .proto 定义,还得把表格里的数据填进去并序列化。这一步通常借助 protobuf 的动态消息或者先编译 .proto 再反射填充。为了不依赖编译步骤,用动态方式更灵活。

from google.protobuf import descriptor_pb2, descriptor_pool, message_factory def build_message_class(proto_text): """用动态方式根据 proto 文本构建消息类""" # 实际工程里一般用 grpc_tools.protoc 先编译,这里示意动态构建思路 # 省略具体 descriptor 拼装细节,核心是拿到 message 类 pass def write_data(message_cls, rows, fields, out_path): """把数据行写入二进制文件""" with open(out_path, "wb") as f: for row in rows: msg = message_cls() for i, field in enumerate(fields): value = row[i] if i < len(row) else "" set_field(msg, field, value) # 每条记录前加长度前缀,方便读取时切分 data = msg.SerializeToString() f.write(len(data).to_bytes(4, "little")) f.write(data)

这里用了一个常见技巧:每条记录前写 4 字节长度。因为 protobuf 序列化后本身不带边界,多条记录连着写,读的时候不知道哪到哪是一条。加长度前缀后,读取端先读 4 字节拿到长度,再读对应字节数,循环即可。set_field是个辅助函数,负责把表格里的字符串按字段类型转成 int、float 或 bool 再塞进消息,字符串转数字失败时给默认值 0,避免整批数据因为一个格子写错而失败。

4. 跑起来:环境准备、命令行参数与导出验证

4.1 环境依赖与安装

工具本身依赖不多,核心就两个:读 xls 的库和 protobuf 库。

pip install xlrd protobuf

注意xlrd从 2.0 版本开始不再支持 xlsx,只支持老式 xls。如果你的表格是 xlsx,要么另存为 xls,要么换openpyxl。这是很多人第一次跑会翻车的地方——装完最新版 xlrd,一读 xlsx 就报错,还以为是代码问题。我一般会在 README 里直接写死版本:xlrd==1.2.0,省得后面的人踩坑。

4.2 命令行参数怎么传

入口一般设计成命令行工具,方便接到构建脚本里。典型参数如下:

参数含义示例
-i输入 xls 路径-i config/item.xls
-o输出目录-o out/
-s指定 sheet,不传则全部-s ItemConfig
-n消息名前缀-n Item
python main.py -i config/item.xls -o out/ -s ItemConfig -n Item

跑完之后out/目录下会多出ItemConfig.proto和ItemConfig.bytes两个文件。-n参数控制生成的消息名前缀,比如表格叫item.xls,加上前缀就生成ItemConfig这个消息名,方便多个表统一命名风格。

4.3 验证导出结果对不对

导出完不能直接信,得验证。最直接的办法是写个小脚本把 .bytes 读回来打印几条:

def read_back(message_cls, path, count=3): """读回二进制文件,打印前几条记录""" with open(path, "rb") as f: for _ in range(count): len_bytes = f.read(4) if not len_bytes: break length = int.from_bytes(len_bytes, "little") data = f.read(length) msg = message_cls() msg.ParseFromString(data) print(msg)

如果打印出来的字段值和 Excel 里对得上,说明链路通了。对不上就分两段查:先看读表阶段拿到的行数据对不对,再看序列化阶段字段有没有塞错。这种分段验证的习惯能省很多时间,不然一个字段错位能查半天。

5. 避坑与常见问题:那些导出时才暴露的坑

5.1 现象:导出的数字全变成 0

原因通常是类型映射没对上。表格里类型写的是 “int”,但映射表里只认 “int32”,TYPE_MAP.get返回了默认的string,而字符串 “123” 塞进 int 字段时转换失败,被兜底成了 0。解决方法是把映射表补全,并且在set_field里加日志,转换失败时打印行号和列名,别默默吞掉。

5.2 现象:中文乱码

xls 里中文读出来是乱码,多半是编码问题。老式 xls 内部用 Unicode 存储,xlrd读出来一般是正常的,但如果中间经过了 csv 中转或者用了错误的编码打开,就会乱。解决方法是全程用xlrd直接读,不要先转 csv;写文件时统一用二进制模式,protobuf 序列化本身是字节流,不涉及文本编码。

5.3 现象:字段编号冲突导致新旧数据不兼容

有人为了“好看”,把字段按字母顺序重排,编号也跟着重排,结果老客户端读新数据全错位。protobuf 的铁律是:字段编号一旦发布就不能改,加字段只能往后加编号,删字段要把编号保留(用reserved)。工具层面可以在生成 .proto 时对比上一次的编号记录,发现编号变动就报警。

5.4 现象:空行或合并单元格导致多出脏数据

Excel 里经常有合并单元格,读出来只有第一格有值,其余是空字符串。如果工具不处理,会生成一堆空记录。解决方法是在读表阶段判断整行是否全空,全空跳过;对于合并单元格,约定策划不要跨数据行合并,或者在工具里做向上填充。这个坑没有银弹,只能靠约定加校验。

5.5 现象:导出速度慢,几千行要等很久

如果每写一条记录都重新打开文件或者重新构建消息类,速度会非常慢。优化点有两个:消息类只构建一次,循环里复用;文件用with open打开一次,循环里连续写。另外xlrd读大表本身不快,如果表格超过几万行,考虑先转成中间格式再处理。

6. 二次开发:把工具改造成适合自己项目的形态

这个工具最大的价值不是它现在能干什么,而是它留了多少可改的口子。我拿它做过几次改造,分享几个方向。

第一个方向是加自定义类型。比如项目里经常有 “坐标” 这种由 x,y 组成的字段,表格里写 “100,200”,希望生成 protobuf 里的嵌套 message。改造点是在type_mapper.py里加一个分支,识别到 “vector2” 就生成一个Vector2子消息,set_field里按逗号切分再填 x、y。这样策划填表不用变,程序侧拿到的就是结构化数据。

第二个方向是接构建流水线。把命令行调用包一层,在 CI 里监听 xls 目录变化,一有提交就自动导出并跑一遍校验。校验包括:字段编号是否和上次一致、必填字段是否有空值、数组字段格式是否正确。这一步做完,配置表出错基本在提交阶段就能拦住,不用等到运行时。

第三个方向是支持多语言输出。同一份 xls,除了生成 protobuf,还可以顺手生成一份 JSON 给前端调试用,或者生成一份 C# 常量类给客户端。做法是在生成层加一个 writer,复用解析层和映射层的结果,输出格式不同而已。

改造的时候有个习惯我一直保持:每加一个功能,先在config/mapping.json里加配置项,而不是硬编码在代码里。这样别人接手时,看配置文件就知道支持哪些类型、哪些规则,不用翻源码。从那以后我每次改这类工具,都强制先想“这个规则能不能配置化”,能配置的绝不写死。希望这个思路帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询