
简介这是一款基于Protocol Buffers的Excel表格解析工具可将xls配置表直接转换为Unity可用的C#数据结构面向游戏客户端开发人员和数值策划解决配置频繁变动时手动同步数据与代码的难题也适合想了解配置驱动开发思路的人群。压缩包内含34个文件以12个C源文件和12个头文件组成核心解析模块1个Python脚本承担转换入口3个SConstruct文件管理跨平台构建另有Go文件、2份Markdown说明、文本文件及示例xls表整体仅375KB轻量精悍便于阅读与二次开发。目前已有659人学习。整个工程演示了从Excel表格到protobuf消息、再到Unity C#类的完整链路并借助protobuf-net打通了C#侧的数据绑定通过阅读源码和示例可学习跨语言序列化设计、C/Python/Go混合工程的组织方式以及游戏配置表自动化生成管线的落地经验。适合有一定C或Python基础、希望提升游戏开发数据管理效率的中级开发者。1. 这个工具到底解决什么问题1.1 研发协作中的Excel与protobuf矛盾做客户端或者服务端开发的同学尤其是游戏行业的肯定对这套流程不陌生策划在Excel里维护数值、技能表、掉落表程序这边拿着配表去写逻辑。Excel是人眼友好的但程序运行时要的是紧凑、高效、可跨语言的序列化数据protobuf正好是干这个的。问题就出在两个格式之间的“翻译”上。我以前见过不少团队策划改了数值程序还得手动把数据拷贝到代码里或者写死JSON让客户端去解析。小表还好一旦遇到几百列、上万行的配置表手工处理就是灾难。更难受的是字段一旦对不上、类型写错运行期才暴露定位问题能折腾一晚上。所以“protobuf解析xls工具”这个需求本质上是想解决Excel配置表到protobuf对象之间的自动映射与序列化让两边各干各的程序这边只管定义好结构和加载逻辑策划那边只管填Excel。1.2 工具核心能力拆解我把它拆开看需要具备这几个能力读取xls格式的Excel文件按行列拆出单元格数据。根据预先定义的proto文件生成对应的数据结构代码比如Python类或C类。把Excel每一行数据填充到对应结构体里处理字段名映射、类型转换、嵌套消息这些细节。最终序列化成二进制文件.bytes、.pb等供运行时高效加载。说白了它就是一个“配置表编译工具”把人类可读的表格编译成机器友好的二进制。理解了这层逻辑你就知道代码该怎么组织遇到需求变化也不慌。2. 技术选型为什么是这个组合2.1 解析xls的方案对比先说读取Excel。现在xls这种老格式其实挺头疼的它是二进制BIFF格式各家工具支持程度不一样。网上搜“解析xls”能看到大量方案但真正靠谱的不多。用Python的话xlrd是最经典的注意它的新版本只支持xls不支持xlsx所以如果你手里的文件是xlsx就别用它了换openpyxl或者pandas openpyxl引擎。我把几个常见方案放一起对比过方案支持格式性能易用性适合场景xlrdPython仅xls老版本同时支持xlsx但新版砍了快简单直接老项目、xls文件多openpyxlPythonxlsx为主中等简单新项目、xlsx文件多pandas两者都行底层调库中等内存占用高最省事数据分析和快速验证Apache POIJavaxls xlsx内存占用大但功能全较繁琐Java技术栈需要写大量自定义逻辑Node.js xlsx库两者都行中等还行前端、全栈工具链我个人推荐Python的原因很简单生态好、开发速度快、protobuf支持完善。xlrd读取xls时要注意它返回的单元格类型有数字、文本、日期、布尔等日期会被转成浮点数这个后面讲坑的时候细说。2.2 protobuf生成代码的关键点protobuf本身的版本选择也要留意。现在主流是proto3语法相比proto2少了required/optional这些修饰符字段默认就是“可缺省”加上默认值机制写配置表时容错性更高。我的建议是新项目一律proto3老项目如果还在用proto2那工具代码要做兼容处理别乱改proto语法否则编译直接报错。生成代码的方式有两种手动下载protoc编译器执行命令行生成。使用Python库grpcio-tools里面带了protoc编译器可以不用额外安装环境。grpcio-tools这种方式对工具链非常友好一条pip命令装完写脚本时直接调用grpc_tools.protoc模块就能编proto不用让同事去配PATH环境变量。这个选择在那个“android protobuf框架引入”热搜词里也有体现很多项目都开始用这种方式管理proto生成流程确实省心。3. 核心流程与关键代码实现3.1 定义proto文件一切的基础工具的第一步是有一份约定好的proto契约。比如策划要配置一张道具表我们先定义一个id为键的消息结构// item_config.proto syntax proto3; package config; message ItemCfg { int32 id 1; // 道具ID string name 2; // 道具名称 int32 type 3; // 道具类型 int32 price 4; // 购买价格 int32 sell_price 5; // 出售价格 bool can_sell 6; // 是否可出售 repeated int32 attr 7; // 附加属性列表 } message ItemCfgTable { mapint32, ItemCfg items 1; }这里我故意加了一个repeated字段和map类型因为这是配置表里最常用的两种结构解析时最容易踩坑。整个表用map包一层运行期就可以按id直接查数据性能和写法都舒服。3.2 用protoc生成Python代码在项目根目录执行protoc --python_out./gen ./proto/item_config.proto如果用grpcio-toolsimport grpc_tools.protoc as protoc protoc.main([ protoc, --python_out./gen, --proto_path./proto, ./proto/item_config.proto ])执行完之后gen目录下会多出item_config_pb2.py这就是我们后续要import的Python模块。里面有两个关键类ItemCfg单条配置消息对象。ItemCfgTable整个表的容器消息。它们的用法非常直接from gen import item_config_pb2 cfg item_config_pb2.ItemCfg() cfg.id 1001 cfg.name 传说宝箱 cfg.attr.append(5) cfg.attr.append(10) print(cfg)这段代码在验证proto定义是否合理时很有用建议拿到proto文件先写个这样的小demo跑一遍确认字段编号和类型没有冲突再往下做Excel映射。3.3 读取xls文件并映射到pb结构接着是核心逻辑读取xls把每一行转成ItemCfg对象。这里我以xlrd为例完整代码给大家参考import xlrd from gen import item_config_pb2 def parse_xls_to_proto(file_path): workbook xlrd.open_workbook(file_path) sheet workbook.sheet_by_index(0) # 取第一个sheet table item_config_pb2.ItemCfgTable() # 假设第一行是字段名第二行开始是数据 headers [sheet.cell_value(0, col) for col in range(sheet.ncols)] for row in range(1, sheet.nrows): item table.items[row] # 注意这里map的key直接用行号仅演示 for col, field_name in enumerate(headers): value sheet.cell_value(row, col) setattr(item, field_name, value) # 序列化成二进制 with open(item_config.bytes, wb) as f: f.write(table.SerializeToString()) print(f已生成 {len(table.items)} 条配置)这个实现能跑但离“可用”还差得很远。实际项目中表头往往不止字段名还有类型声明、注释说明、分组标记等下面我给大家写一个更贴近生产环境的版本。3.4 完善版解析逻辑生产环境里我习惯把Excel的表头做三行约定第一行字段名。第二行字段类型帮助工具做转换。第三行注释给策划看的功能说明。工具只解析第一行和第二行第三行直接跳过。这是业内很主流的配置表规范策划也好理解。import xlrd from gen import item_config_pb2 from google.protobuf.descriptor import FieldDescriptor TYPE_MAP { int32: FieldDescriptor.TYPE_INT32, int64: FieldDescriptor.TYPE_INT64, string: FieldDescriptor.TYPE_STRING, bool: FieldDescriptor.TYPE_BOOL, float: FieldDescriptor.TYPE_FLOAT, double: FieldDescriptor.TYPE_DOUBLE, } def convert_value(field_desc, raw_value): 根据字段的protobuf类型转换Excel单元格值 if field_desc.type in (FieldDescriptor.TYPE_INT32, FieldDescriptor.TYPE_INT64): return int(float(raw_value)) # Excel里数字可能被读成float if field_desc.type FieldDescriptor.TYPE_STRING: return str(raw_value) if field_desc.type FieldDescriptor.TYPE_BOOL: if isinstance(raw_value, str): return raw_value.strip().lower() in (true, 1, yes) return bool(raw_value) return raw_value def parse_excel_to_pb(excel_path, proto_module, table_cls, sheet_index0): workbook xlrd.open_workbook(excel_path) sheet workbook.sheet_by_index(sheet_index) headers [] types [] for col in range(sheet.ncols): headers.append(str(sheet.cell_value(0, col)).strip()) types.append(str(sheet.cell_value(1, col)).strip()) table table_cls() for row in range(2, sheet.nrows): # 用第一列作为map的key按需调整 key int(sheet.cell_value(row, 0)) msg table.items[key] for col, field_name in enumerate(headers): if not field_name: continue desc msg.DESCRIPTOR.fields_by_name.get(field_name) if desc is None: continue raw sheet.cell_value(row, col) if desc.label FieldDescriptor.LABEL_REPEATED: # repeated字段在Excel里通常用逗号分隔 values str(raw).split(,) for v in values: getattr(msg, field_name).append(convert_value(desc, v.strip())) else: setattr(msg, field_name, convert_value(desc, raw)) return table这段代码的核心思路是用protobuf自身的字段描述信息Descriptor来驱动映射而不是硬编码每个字段。好处有二一是proto文件里加字段Excel里加一列即可工具代码不用动二是类型转换可以统一在这一个函数里兜底减少到处写重复逻辑。注意map类型在Python的protobuf实现里用table.items[key]这种方式会给key凭空创建空消息这很方便但如果你第一次赋值不是修改而是读取要留意会不会误生成空条目。4. 实战中踩过的坑4.1 字段类型不匹配Excel数字全变floatxlrd读整数单元格时返回的是float类型比如100被读成100.0。如果你直接把100.0塞给protobuf的int32字段Python的protobuf实现会报类型错误。我在代码里用int(float(raw_value))做了一次强制转换这才稳了。字符串类型也一样如果你在Excel里填的是数字但字段定义是string不转换直接赋值会直接崩。所以convert_value这个函数里类型转换一定要做全。4.2 repeated字段的解析策略配置表里最常见的repeated形式有两种同一列用逗号/分号分隔多个值。多列用相同前缀表示同一个list。第一种我用上面的代码已经处理了按分隔符拆开再逐个填。第二种常见于技能效果、掉落概率这种“变长”结构字段名类似attr_1、attr_2我的建议是这种场景不要硬塞到proto的repeated里改成嵌套message更清晰。比如message AttrItem { int32 type 1; int32 value 2; } repeated AttrItem attrs 7;这样每个子结构在Excel里有两列attrs_1_type、attrs_1_value解析时按列名后缀归组虽然解析代码复杂一点但配表的可读性大大提升。4.3 xls与xlsx兼容性标题写的是“xls”但现在很多项目拿到的表其实是“xlsx”。如果你用老版xlrd 0.9.0去开xlsx会直接抛异常。我用的时候是直接做了两个分支.xls走xlrd.xlsx走openpyxl统一封装成一个load_excel_data()函数外部调用无感知。这样不用纠结“到底是哪种格式”工具健壮性也更好。4.4 日期格式和空值Excel里日期单元格读出来是浮点数需要格式化。比如import xlrd cell_value sheet.cell_value(row, col) if sheet.cell_type(row, col) xlrd.XL_CELL_DATE: cell_value xlrd.xldate_as_datetime(cell_value, workbook.datemode)空单元格读出来是空字符串或者None如果不处理赋给protobuf的int32字段会报错。我的方案是在convert_value里加一个raw_value为空就跳过赋值的判断保持proto默认值。顺手整理一个排查速查表遇到问题先对号入座现象可能原因解决办法导入pb2模块报错proto语法版本与protoc版本不匹配升级protoc或用grpcio-tools统一编译int类型字段不能赋floatExcel单元格默认返回浮点数转换时包一层int(float(...))map取key报KeyError空行被当作一条数据跳过全空行只在第一列有值时创建条目repeated字段解析少数据分隔符不一致中英文逗号混用统一用replace(, ,)预处理生成的文件运行时读不了proto文件与解析工具用的proto版本不一致proto文件的message定义不要随意改字段编号5. 工具链扩展思路5.1 做校验与diff工具光有“解析”还不够配置表的价值在于正确性。解析完成后可以顺手做几件事检查是否有关键字段为空或超出枚举范围。对比两次生成的二进制文件输出diff日志方便策划知道改了哪些配置。这个做法的效率提升非常明显。我以前经历过一个版本某个道具价格配错上线才被发现修复成本远超当时“写个校验规则”花的那几分钟。5.2 客户端与服务端共用配置proto本身就是跨语言的。同一个proto文件客户端用C或C#生成代码服务端用Java或Go生成代码工具这边一次解析生成二进制两端都能读。重点在于二进制产物要作为构建产物管理不要每次启动时现算。也就是要有一条“配表修改 - 跑工具 - 产出bytes - 提交到版本库”的流程这样运行时的加载代码可以做得极简。5.3 可视化配置与热更新如果项目里策划对Excel依赖很深可以做一层简单的“配置编辑器”Web界面背后还是这个解析工具。策划在线编辑工具实时产出二进制版本库自动更新。终端玩家下一次版本就能用上最新配置。这个扩展方向我建议在工具跑通后逐步加不要一上来就做先把主流程的可靠性打磨好。6. 一些个人经验总结做这类工具我最大的体会是不要追求一步到位写一个“万能”工具先解决当前项目最痛的那张表再逐渐抽象。一开始就想着支持所有数据类型、所有Excel格式、所有嵌套结构大概率会把项目拖垮。我是从一张道具表开始的跑通之后再抽公共逻辑前前后后改了三版最终才稳定下来。另外proto文件的字段编号一旦定下来就尽量不要改这是protobuf的基本礼仪。否则线上数据和老版本代码全乱套这个坑比解析Excel本身的坑大得多。最后分享一个我常用的调试技巧写一个临时脚本把解析出的message用print(msg)打出来人工核对几条关键记录。protobuf的打印格式非常友好一眼就能看出字段映射对不对比自己写debug日志高效很多。工具虽小但它卡在策划和程序之间做好了整个团队的配置迭代速度都能上一个台阶。本文还有配套的精品资源点击获取