excelexporter
excelexporter copied to clipboard
这是一个可以将策划编辑的Excel表导出成各种目标文件的工具
Excel Exportor 游戏策划导表工具
这是一个可以将策划编辑的Excel表导出成各种目标文件的工具,它是用Python3写的,所以只要你的系统有Python3,那么不管是Windows,还是Linux,或者MacOS,都可以正常使用。
这个工具最早为python2所写,但是python2在字符编码方面存在一些缺陷,加之python2今年已经不再维护了,所以我把它改成只支持python3,并把它开源出来。
除了python3,还依赖于sxl
,这个库用于读Excel的数据:
sudo pip3 install sxl
使用指南
Excel Exportor
涉及到三种文件:
-
Excel文件
,这是策划编辑的各种游戏配置文件,也是我们要导的源文件。 -
定义文件
,这是一个py文件,由开发人员编辑,一个定义文件只能导出Excel的一个Sheet,里面定义了要导出哪个Excel文件,哪个Sheet,哪些列,这些列的数据类型;以及要导成什么目标文件。 -
目标文件
,导出来的配置文件,给程序使用,当前支持:lua, json, js, py
。
命令行帮助如下:
usage: excel_exporter.py [-h] -p PATH [-a ALL]
Excel Expoter
optional arguments:
-h, --help show this help message and exit
-p PATH, --path PATH 指定导表定义文件的路径,可以是具体的文件,也可以是一个目录
-a ALL, --all ALL 是否全导,如果不,则只导修改过的文件
注意-p
指定的是定义文件的路径,你可以指定一个定义文件的全路径,也可以指定一个目录,当为目录时,目录里面的py文件都被认为是定义文件。下面是几个例子:
# 导出test目录里的定义文件,并且强制全导
python3 excel_exporter.py -p ./test -a 1
# 只导一个具体的定义文件
python3 excel_exporter.py -p ./test/测试文件1.py
-a
如果不指定,则只导那些修改过的,哪些算修改过的呢:
- 要导出的目标文件不存在。
- Excel文件或定义文件的修改时间比目标文件新。
这个特性是非常有用的,有些重型游戏可能达到数百个配置表,每次只导那些修改过的文件可以节省很多时间。如果需要强制全导只要指定-a 1
即可。
定义文件格式
通过上面的介绍可知,这个工具最重要的部分就是定义文件,具体的格式如下:
# -*- coding: utf-8 -*-
from excel_exporter import *
# 这里定义要导出哪些字段,每一行代表表格的一列。每一行有三项。
# 第一项为表格列名,第二项为导出的字段名,第三项为该字段的类型
define = [
['编号', 'no', Int()], # 整型,表格中不填为0
['整型', 'i', Int()],
['浮点数', 'fl', Float()], # 浮点数,表格中不填为0
['字符串', 'str', Str()], # 字符串
['布尔值', 'bo', Bool()], # 布尔值,表格中不填或填0为false, 填1为true。
['列表', 'li', List(Str())], # 列表类型,第1个参数指定元素的类型,这里为字符串;第2个参数为分隔符,默认是|,这里为默认
['列表2', 'li2', List(List(Int(), ","), "|")], # 列表的元素还可以是列表,这里展示了复杂类型的定义
['元表', 'tu', Tuple(",", Int(), Str())], # 元素类型,第1个参数为分隔符,后面是每个元素的类型,
# 这里表示有两个元素,一个是整型,一个是字符串。
['字典', 'di', Dict(Str(), Float())], # 字符串类型,第1个参数为Key的类型,第2个参数为Value的类型,第3个参数为KV对的分隔
# 符,默认是|;Key和Value的分隔符强制是: 一个例子: atk:100|def:20|bj:30
]
config = {
# 这是Excel文件的路径,它可以是一个绝对路径,也可以是相对于该定义文件的路径。
"source": "测试文件1.xlsx",
# 要导出的Sheet
"sheet": "Sheet1",
# 导出的目标文件
"target": [
["./out/test1.js", "js", ET_OPT], # 导出JS,ET_OPT表示对导出结构作优化,不指定则用简单的字典格式
["./out/test1.lua", "lua", ], # 导出LUA,ET_OPT表示对导出结构作优化,不指定则用简单的字典格式
["./out/test1.json", "json"], # 导出JSON,Json不能指定ET_OPT
["./out/test1.py", "py"], # 导出PY,PY不能指定ET_OPT
],
# 指定键名,键字段的值必须唯一,不能重复
"key": "no"
}
# 可选:自定义每一行Key的值
# def custom_key(key_value, row_dict):
# return "{}_{}".format(row_dict["job"], row_dict["level"])
# 可选:自定义每一行的值
# def custom_row(key_value, row_dict):
# if not (1 <= row_dict["job"] <= 3):
# raise TypeError("职业字段只能1,2,3")
# return row_dict
# 可选:检查导出的表是否正确
# def verify_table(table):
# pass
该项目的test目录中有一些测试文件,下面是导出的Lua的样子:
-- 测试文件1.xlsx:Sheet1
-- no = 编号
-- i = 整型
-- fl = 浮点数
-- str = 字符串
-- bo = 布尔值
-- li = 列表
-- li2 = 列表2
-- tu = 元表
-- di = 字典
local test1 = {
[1] = {bo = false, di = {atk = 10.0, bj = 0.2, def = 20.0}, fl = 0.968747514728312, i = 59, li = {"名字1", "名字2", "名字3"}, li2 = {{100, 10}, {100, 102}, {100, 103}}, no = 1, str = "1级暴击宝石", tu = {1, "苹果"}},
[2] = {bo = true, di = {atk = 10.0, bj = 0.3, def = 20.0}, fl = 0.916421476611288, i = 46, li = {"名字1", "名字2", "名字4"}, li2 = {{100, 10}, {100, 102}, {100, 104}}, no = 2, str = "2级暴击宝石", tu = {2, "火龙果"}},
[3] = {bo = false, di = {atk = 10.0, bj = 0.4, def = 20.0}, fl = 0.575660861787979, i = 54, li = {"名字1", "名字2", "名字5"}, li2 = {{100, 10}, {100, 102}, {100, 105}}, no = 3, str = "\"3级暴击宝石\"", tu = {2, "苹果"}},
[4] = {bo = false, di = {['攻击'] = 10.0, ['暴击'] = 0.5, ['防御'] = 20.0}, fl = 0.074234925210084, i = 54, li = {"名字1", "名字2", "名字6"}, li2 = {{100, 10}, {100, 102}, {100, 106}}, no = 4, str = "4级暴击\n宝石", tu = {3, "火龙果"}},
[5] = {bo = true, di = {['攻击'] = 10.0, ['暴击'] = 0.6, ['防御'] = 20.0}, fl = 0.00521433361322998, i = 12, li = {"名字1", "名字2", "名字7"}, li2 = {{100, 10}, {100, 102}, {100, 107}}, no = 5, str = "5级暴击宝石", tu = {3, "苹果"}},
[6] = {bo = false, di = {['攻击'] = 10.0, ['暴击'] = 0.7, ['防御'] = 20.0}, fl = 0.155339775808538, i = 77, li = {"名字1", "名字2", "名字8"}, li2 = {{100, 10}, {100, 102}, {100, 108}}, no = 6, str = "6级暴击宝石", tu = {0, ""}},
[7] = {bo = false, di = {}, fl = 0.0, i = 0, li = {}, li2 = {{100, 10}, {100, 102}, {100, 109}}, no = 7, str = "7级暴击宝石", tu = {0, ""}},
[8] = {bo = true, di = {}, fl = 0.0, i = 0, li = {}, li2 = {{100, 10}, {100, 102}, {100, 110}}, no = 8, str = "8级暴击宝石", tu = {0, ""}},
}
return test1
Lua和JS可以通过指定ET_OPT
来输出优化过的目标文件,来看看一个Lua配置优化后的样子:
-- 测试文件1.xlsx:Sheet1
local key_map = {
-- 编号
no = 1,
-- 整型
i = 2,
-- 浮点数
fl = 3,
-- 字符串
str = 4,
-- 布尔值
bo = 5,
-- 列表
li = 6,
-- 列表2
li2 = 7,
-- 元表
tu = 8,
-- 字典
di = 9,
}
local test1 = {
[1] = {1, 59, 0.968747514728312, "1级暴击宝石", false, {"名字1", "名字2", "名字3"}, {{100, 10}, {100, 102}, {100, 103}}, {1, "苹果"}, {atk = 10.0, bj = 0.2, def = 20.0}},
[2] = {2, 46, 0.916421476611288, "2级暴击宝石", true, {"名字1", "名字2", "名字4"}, {{100, 10}, {100, 102}, {100, 104}}, {2, "火龙果"}, {atk = 10.0, bj = 0.3, def = 20.0}},
[3] = {3, 54, 0.575660861787979, "\"3级暴击宝石\"", false, {"名字1", "名字2", "名字5"}, {{100, 10}, {100, 102}, {100, 105}}, {2, "苹果"}, {atk = 10.0, bj = 0.4, def = 20.0}},
[4] = {4, 54, 0.074234925210084, "4级暴击\n宝石", false, {"名字1", "名字2", "名字6"}, {{100, 10}, {100, 102}, {100, 106}}, {3, "火龙果"}, {['攻击'] = 10.0, ['暴击'] = 0.5, ['防御'] = 20.0}},
[5] = {5, 12, 0.00521433361322998, "5级暴击宝石", true, {"名字1", "名字2", "名字7"}, {{100, 10}, {100, 102}, {100, 107}}, {3, "苹果"}, {['攻击'] = 10.0, ['暴击'] = 0.6, ['防御'] = 20.0}},
[6] = {6, 77, 0.155339775808538, "6级暴击宝石", false, {"名字1", "名字2", "名字8"}, {{100, 10}, {100, 102}, {100, 108}}, {0, ""}, {['攻击'] = 10.0, ['暴击'] = 0.7, ['防御'] = 20.0}},
[7] = {7, 0, 0.0, "7级暴击宝石", false, {}, {{100, 10}, {100, 102}, {100, 109}}, {0, ""}, {}},
[8] = {8, 0, 0.0, "8级暴击宝石", true, {}, {{100, 10}, {100, 102}, {100, 110}}, {0, ""}, {}},
}
do
local item_metatable = {
__index = function (t, k)
local pos = key_map[k]
if pos then
return rawget(t, pos)
else
return nil
end
end,
}
local setmetatable = setmetatable
for _, date_item in pairs(test1) do
setmetatable(date_item, item_metatable)
end
end
return test1
稍微熟悉Lua的人马上就能看出,字典中的项全部变成数组的形式,然后通过元表机制,仍然可以像这样使用:
local test1 = require "test1"
local item = test1[1]
print(item.fl, item.str)
由于JS没有类似元表的机制,所以很难做到像上面那样调用,只能这样写:
let test1 = require("./test1")
let item = test1[1]
console.log(item.get("f1"), item.get("str"))
一些技巧
- 对于同一个配置文件,服务器和客户端要导出的列不同,该怎么做?
写两份定义文件,一份是服务器的,一份客户端的。实际上最佳实践应该是服务器维护一个目录,客户端维护一个目录,这样两边就能自由发挥,互不影响。
- 对导出的配置表进行一些检查。
定义文件中有一个可选的custom_row
函数,每取出一行就会调用一次,在这里可以对每一行作一些合法性检查,如:
def custom_row(key_value, row_dict):
if not (1 <= row_dict["job"] <= 3):
raise TypeError("职业字段只能是1,2,3")
return row_dict
你也可以在这个函数里对一行数据进行定制,这种需求很少见。
当取出所有行之后,会调用verify_table
函数,也可以在这里作一些检查。
后话
这个工具在我们的项目中已经使用多时,目前支持的文件类型就这些,如果需要支持其他目标文件,欢迎提PR。