第二篇文章是对流程的概述,从第三篇文章开始编辑打表的具体细节
策动配表习惯应用excel,咱们打表指标也是xlsm和xlsx文件
开始打表前须要确认好打表工具目录在哪,能够在所有Excel配表同层级的文件夹内新建个文件夹命名TableCreater
TableCreater里有几个目录要辨别分明,首先是python脚本文件夹叫Scripts,还有在运行工具期间生成的各种文件,用Temp文件夹存储,Temp里有PB_Python、PB_lua、Proto、Bytes等文件夹,打表流程启动后生成的各种文件要放到对应文件夹中
Scripts文件夹内要有start.py、main.py、excelToCSV.py、export_proto.py、export_pb_lua.py、export_pb_python.py、export_bytes.py、export_bytesTables.py
Scripts文件夹内的文件一看就是用来打表的各个流程啦。咱们能够在start.py的main函数中按步骤执行即可
尽管我的项目是从start.py开始启动的,但具体的打表流程咱们放到main.py中运行,start.py的职责是监听用户输出信息并收集信息塞给main.py进入打表流程
如下图所示,start.py的性能有tab键补齐名字,分号多个输出,只是输出all示意全副excel打表,以及clear清空log等操作。
如图所示,我输出了一个115_资源总表,在start.py里监听输出并主动把全门路拼齐,当按下回车时,start.py把【115_资源总表】这个名字传给main.py开始了打表流程,因为咱们输出了一个表名而不是"all",因而到了main函数中天然不是全打表
import os#读行模块import readline#主动补全模块import rlcompleter#监听用户输出模块import userInputimport sys#开发者自定义的零碎参数import env_debug def getAllExportExcelNames(): inputStr = "" isAll = False if env_debug.isRuning: if len(sys.argv) > 0: inputStr,isAll = userInput.getAllExportExcelNames(sys.argv[13]) else: print("env_debug error") else: #readline模块与colorama模块有抵触,无奈一起应用 py = os.path.abspath("TableCreater/Python27/python") tips = os.path.abspath("TableCreater/Script/tips.py") os.system("{0} {1} 1".format(py,tips)) while True: inputStr,isAll = userInput.getInput() if inputStr == "exit": os.system("exit") break elif inputStr == "clear": os.system("cls") py = os.path.abspath("TableCreater/Python27/python") tips = os.path.abspath("TableCreater/Script/tips.py") os.system("{0} {1} 1".format(py,tips)) elif inputStr == "error": print("command error") else: break return inputStr,isAll #excelNames:要导出的所有Excel表格名称#isAll:是否是全副导出#isVersion:是否是出版本def export(excelNames,isAll,isVersion=False): import main main.run(excelNames,isAll,isVersion) if not isVersion: os.system("pause") def isVersion(): return len(sys.argv) > 0 and sys.argv[len(sys.argv) - 1] == "version" if __name__ == '__main__': env_debug.switch("common") caller = sys.argv[1] if caller == "1": #惯例导表 if not isVersion():#显示用户输出 excelNames,isAll = getAllExportExcelNames() if excelNames != "exit": export(excelNames,isAll) else: #间接导出所有 excelNames,isAll = userInput.getAllExportExcelNames("all") export(excelNames,isAll,True) elif caller == "2": #C#导表 excelNames,isAll = userInput.getAllExportExcelNames(sys.argv[10]) export(excelNames,True,isVersion()) elif caller == "3": #本地战斗服务器专属导表 excelNames,isAll = userInput.getAllExportExcelNames(sys.argv[8]) export(excelNames,True,isVersion())
最下面有个 import env_debug模块,这是咱们本人写的零碎变量定义文件 env_debug.py文件里,这里有必要解释一下为何自定义一个文件
[第一步]:将excel文件转CSV并输入到CSV目录
start.py执行excelToCSV.excute()
为何要把excel转csv,excel文件蕴含了windows的很多库,文件特地大,但csv文件是纯文本文件, 每行数据用逗号','分隔,存储空间小。
def execute(excelName): xlrd.Book.encoding = "gbk" excelPath = setting.dirPath_excel + excelName excel = xlrd.open_workbook(excelPath) #关上Excel for sheet_name in excel.sheet_names(): #遍历子表,一个excel可能有多个sheet页 sheet = excel.sheet_by_name(sheet_name)#拿到sheet数据 #一般来说一个excel有多个页签,每一个页签对应一个proto文件,我的表名称写在第二行第一列 tableName = sheet.cell_value(1,0) csvName = tableName maxCol = 0 #该sheet页最大列数 for i in range(100): #获取以后sheet的最大列数 if sheet.cell(3,i).ctype == 0: maxCol = i break filePath = setting.dirPath_csv + csvName + ".csv" fileObj = codecs.open(filePath,"wb")#该代码会在目标目录创立文件,并设置读写格局 fileObj.seek(0)#从第0行开始写 csv_writer = unicodecsv.writer(fileObj,encoding='utf-8-sig')#unicodecsv是内部库,自行下载导入 tableUser = getTableUser(sheet,endCol) #记录配表使用者,如果没有需要能够不记录,我的我的项目是服务器和客户端共用表,在数据名称的上一行记录该数据由c还是s应用 csv_writer.writerow([tableUser,excelName.decode("gbk"),sheet_name])#写在第一行 rowCount = sheet.nrows #以后sheet最大行数 for i in range(7,rowCount):#正式开始遍历excel每个子表每行数据 #每个子表每列数据 for j in range(0,endCol):#遍历该行每一列数据 #此处把读取数据代码省略,读格子数据的代码为sheet.cell(rowx,colx).value #把你读出来的数据拼接成正确的行 csv_writer.writerow(rowDatas) #按行写入 fileObj.close() #到此一个残缺的XXX.csv文件生成胜利,能够用excel关上查看该文件数据,提醒:关上的数据必须和excel是一个格子一个格子的,行数据不能在一个格子里
start.py执行export_proto.excute()
def execute(csvName): curCsvPath = setting.dirPath_csv + csvName + ".csv" csvfile = codecs.open(curCsvPath,"rb") csv_reader = unicodecsv.reader(csvfile,encoding='utf-8-sig') index = 0 types = [] titles = [] for line in csv_reader: if(index == 0): canExport,msg = Parser.CanExport(line[0]) if(not canExport): debug.throwError("导出proto文件失败:{}.csv{}".format(csvName,msg)) elif(index == 1): userSigns = line #使用者标识 elif(index == 2): #类型 for i in range(0,len(line)): if(Parser.CanUse(userSigns[i])): types.append(line[i]) elif(index == 3): #字段名称 for i in range(0,len(line)): if(Parser.CanUse(userSigns[i])): titles.append(line[i]) else: break index = index + 1 csvfile.close() #code_block,code_struct,code_import = createcode_block(types,titles) code_block,code_struct = createcode_block(types,titles) code = m_code_template.format(code_struct,csvName,code_block,csvName,csvName) codefile = codecs.open(setting.dirPath_proto + "Table_" + csvName + ".proto","wb","utf-8") codefile.write(code) codefile.close()
生成的文件在Temp/Proto文件夹中
start.py执行export_pb_lua.excute(protoName)
调用protoc-gen-lua.bat并传入参数,
其实就是传入各种门路参数,以及 [第二步] 通过数据类型和数据名称构建的Table_XXX.proto 文件门路
lua版pb文件的输入门路等,最初ret如果为true代表生成胜利。
最终输入门路我写的是Temp/PB_Lua,文件名为Table_xxx_pb.lua
这个lua文件就是能够间接在Unity工程中应用的lua版pb,在工具的最初只须要把Temp/PB_Lua文件夹的文件全副copy到工程中即可。
cmd = "{}\protoc-gen-lua.bat {} {} {} {}\Table_{}.proto".format( os.path.abspath(setting.protocPath_lua), os.path.abspath(setting.dirPath_proto), os.path.abspath(setting.dirPath_protoEnum), os.path.abspath(setting.dirPath_pb_lua), os.path.abspath(setting.dirPath_proto), protoName) ret,msg = debug.system(cmd) if(not ret): debug.throwError("{}生成python版pb失败=>\n{}".format(protoName,msg))
目前来看,这些门路的传入程序必须固定,例如proto门路以及想要输入lua版本pb的门路,想要批改的话去改protoc-gen-lua工具的源码
start.py执行export_pb_python.excute(protoName)
调用protoc.exe 并传入参数
和上一步有点类似,重点参数还是 [第二步] 生成的proto文件的门路
生成的文件名称为Table_xxx_py2.py,我的工程输入目录为Temp/PB_Python,目前protoc.exe只能输入python、JAVA和C++三种格局的pb文件,想生成C#等其余格局须要去网上找工具,应用形式和lua版本的一样
cmd = "{}\protoc.exe --proto_path={} --python_out={} --proto_path={} {}\Table_{}.proto".format( os.path.abspath(setting.protocPath), os.path.abspath(setting.dirPath_proto), os.path.abspath(setting.dirPath_protoEnum), os.path.abspath(setting.dirPath_pb_python), os.path.abspath(setting.dirPath_proto), protoName) ret,msg = debug.system(cmd) if(not ret): debug.throwError("{}生成python版pb失败=>\n{}".format(protoName,msg))
[第五步]:csv生成bytes文件导入Unity工程期待读数据
这一步过程比较复杂,代码中波及到屡次数据构建,先简略说思路
咱们提前把这个python文件的构造当做str写好,这个python文件是个模板,也就是上面模块一代码
咱们在模块二的逻辑代码中遍历csv的行数据,把行数据处理成一个str数据后塞入到这个模块一的python代码中,并把这段代码生成一个py文件期待第六步,运行生成的py文件
#模块一!!字符串m_code_template 是一段python文件代码,外面缺失了一些文件数据,比方文件门路、文件名称等,补全信息并且间接调用python.exe执行这段代码即可生成bytes文件m_code_template = u'''#! python2#coding:utf-8import osimport tracebackimport sys reload(sys) sys.setdefaultencoding('utf8')sys.path.insert(0, os.path.abspath('./TableCreater/Script'))import Parserimport settingimport debugsys.path.insert(0, os.path.abspath(setting.dirPath_pb_pythonEnum))sys.path.insert(0, os.path.abspath(setting.dirPath_pb_python))import Table_{}_pb2 def export(): tableData = Table_{}_pb2.Table_{}() BYTES_PATH = {} +"{}.bytes"{} bytes = tableData.SerializeToString() NEWFILE = open(BYTES_PATH,"wb") NEWFILE.write(bytes) NEWFILE.close() try: export()except: debug.error(traceback.format_exc())'''
上面是模块二代码
#模块二 上面这段代码是补全 m_code_template的信息,并生成一个用于转bytes的python文件 for line in csv_reader: index = index + 1 if(index == 0): user = line[0] elif(index == 1): userSigns = line elif(index == 2): types = line elif(index == 3): titles = line else: code_row = createcode_row(line,types,titles,userSigns,tableName) allRowStrs.append(code_row) allRowStrs.append("\n") # parser_code = parser_code + code_row + "\n" # print(index) #print("这里") parser_code = ''.join(allRowStrs) exportPath = "setting.dirPath_byte" if user == "client": exportPath = "setting.dirPath_byte_clientOnly" code = m_code_template.format(tableName,tableName,tableName,exportPath,tableName,parser_code) csvfile.close() c odefile = codecs.open(setting.dirPath_code + "Table_" + tableName + ".py","wb","utf-8-sig") #创立一个python文件,并把数据写入到该python文件中 codefile.write(code) codefile.close()
上面把模块二中数据构建的函数
def createcode_row(line,types,titles,userSigns,tableName): code = u"\tdata = tableData.datas.add()\n" for i in range(0,len(line)): if(Parser.CanUse(userSigns[i])): code = code + createcode_col(line[i],types[i],Parser.GetTitle(titles[i]),tableName) return code
贴代码有点不不便用户浏览,先这么整吧
def createcode_col(cell,dataType,title,tableName): code = u"" if(cell != u""): if(Parser.IsArray(dataType)): if(Parser.IsStructArray(dataType)): #print("构造体数组") code = code + createcode_structArray(cell,dataType,title) else: #print("根本数据类型数组") code = code + createcode_baseTypeArray(cell,dataType,title) else: if(Parser.IsStructArray(dataType)): code = code + createcode_struct(cell,dataType,title,tableName) else: if dataType == u'JSON': dataType = u'STRING' if dataType == u'STRING': cell = cell.replace('\"','\\"') #根本数据类型赋值 code = code + u"\tdata.{} = Parser.GetValue(u\"{}\",u\"\"\"{}\"\"\")\n".format(title,dataType,cell) return code
[第六步]:生成Lua读表脚本,业务开发调用该脚本读数据
[第五步]负责把csv数据各种拼凑组合到一个python模板中,而后生成py文件,第六步就轮到调用该py文件啦
#第五步中生成的python文件是可执行的,应用cmd命令执行该python文件即可生成对应的bytes文件cmd = "\"TableCreater\\Python27\\python\" {}Table_{}.py".format(setting.dirPath_code,codeName) os.system(cmd)
这一步会生成咱们想要的bytes文件并输入到Temp/Bytes文件夹中,文件名字为xxx.bytes,这个xxx就是咱们后面的csvName或protoName
[第七步]:生成对立的读表接口
应用python全自动生成一个lua文件用来治理所有的读表性能,因为一个工程的表切实太多了。手写太繁琐,该工具能够叫AllTables.lua,当系统启动时,调用该对立入口,把所有表全副加载入内存,对立治理
此处只举两个表为例,别离是Achievement成就表和Table_ItemInfo道具表,AllTables.lua代码全副由python批量生成,不是手写的,无论有多少个表都能够循环写入,业务开发的人如果想读成就表,则只须要调用Table_Achievement.GetRowData(keyName)即可,之后依据框架设计走同步或者异步加载bytes文件并读取行数据,首次加载必定是要先把bytes数据按KeyName已key、value的形式存下来不便读取
_G.Table_Achievement = { Belong = "common", KeyName = "conditionSid", InitModule = function () require "Logic/Table/AutoGen/TablePb/Table_Achievement_pb" TableCtrl.LoadTable("Achievement") end, Parser_Table = function (bytes) local table = Table_Achievement_pb.Table_Achievement() table:ParseFromString(bytes); return table.datas end, GetRowData = function (id) return TableCtrl.GetRowData("Achievement",id) end, GetAllRowData = function () return TableCtrl.GetAllRowData("Achievement") end,} _G.Table_ItemInfo = { Belong = "common", KeyName = "id", InitModule = function () require "Logic/Table/AutoGen/TablePb/Table_ItemInfo_pb" TableCtrl.LoadTable("ItemInfo") end, Parser_Table = function (bytes) local table = Table_ItemInfo_pb.Table_ItemInfo() table:ParseFromString(bytes); return table.datas end, GetRowData = function (id) return TableCtrl.GetRowData("ItemInfo",id) end, GetAllRowData = function () return TableCtrl.GetAllRowData("ItemInfo") end,} #当系统启动时,调用AllTables的RegisterModulelocal AllTables = { Init = function (RegisterModule) #这个是零碎注册模块的中央 RegisterModule(Table_Achievement,false) RegisterModule(Table_ItemInfo,false)}
[额定常识]
咱们写好的python工程是可运行的,但其余开发人员不心愿关上工程去运行,因而咱们写一个exportToClient.cmd文件执行写好的python工程
@echo off echo "cd /d %~dp0的作用是切换到当前目录"cd /d %~dp0"./export_fight_proto/Python27/python" "./export_fight_proto/start.py":end
打表工具只是出包流程中的其中一步,如果其余cmd文件要调用exportToClient.cmd, cd /d %~dp0这句话的意义就很重要了。切换以后门路
完结语
这套工具是咱们项目组共事写的,这一套打表框架我我只贴了一小部分代码,省略了有数代码,本篇文章只梳理核心思想。前面咱们会聊到对于游戏,手游等等的开发