跳转到内容

效率提升:使用Python配置数据表

唔,首先说一下这是在生产环境,生产环境自然是和自己写着玩的环境是不一样的处理方法,生产环境中我们想要的大概是正确、稳定、高效。

jskyzero 2019/01/18

是这样的,出于某种目的,现在我需要配置某个表格,表格是这样的,一部分是配置了一些人数属性,另一部分则需要配置关卡与人物属性的对应关系,用于在关卡中使用,关卡中是需要使用到三位角色,目前是将48名角色随机挑选三位给了关卡,但是这样可能会出现三位某些属性相同的角色的情况,为此需要修改配置。

目前支持的方式是,从若干角色中挑选三位,和若干相同关卡对应角色中,随机一种对应,在和相关同(前)学(辈)沟通过以后拒绝在程序部分增加逻辑,希望维持原有配置方式,考虑到之前是采用第一种方式配置,这次需要采用第二种减少挑选范围,扩大同可能挑选对应方式。

但是仔细一想这炸了,这要是枚举出来过百我都懒得配了,之前一个关卡我只写了一行,现在随便要翻若干倍,显然这里靠蛮力是不行的,好在后台逻辑不改,没事,我可以前台手动枚举,这个本质上数学模型还是很简单,具体涉及到一点excel读取,写入,和数据的概率统计处理方面的知识。

再次提及这是在生产环境,甚至没有办法去装某些包,翻了下好歹有个xlrd可以用来读取excel,足够了足够了,暂时是打算写出csv,然后手动拷贝到表中储存,excel中的操作就不太熟悉了,我可以相对模拟输出格式,直接拷贝粘贴,实际上面对几十万行的excel数据,手动修改就会有点不现实了。

那么大概的流程是,读入角色,模拟随机方式,然后去掉不合适的组合,然后写出csv格式文件,然后手工拷贝粘贴至源表格。

  • 引入包,常量定义(字符常量已经删除,反正是无意义的字符序列)
import xlrd # 读取 Excel 工作簿;这里只读不写,避免修改原始配置表。
import csv # 将枚举结果输出为 CSV,方便复制回配置表或做二次处理。
import itertools # combinations/product 用于生成角色组合与笛卡尔积。
ROLE_NUM = 10 # 每组角色数量,用于按角色编号分组切片。
# static string:生产路径和表名被删去,实际使用时需要替换为真实配置。
EXCEL_PATH =
OUTPUT_PATH =
PveRobotConf_Name =
PveRobotAttrs_Name =
PveRobotAttrs_ID_Name =
PveRobotAttrs_JOB_Name =
  • 数据读入

xlrd的方式是先获得book,然后book打开sheet,sheet再获取行、列、或者某个格子,要注意的xlrd只是读取,是没有保存相关接口的。

def read_data():
# 打开 Excel 文件;xlrd 只负责读取,因此不会意外覆盖源配置。
book = xlrd.open_workbook(EXCEL_PATH);
# PveRobotConf 在这段示例中没有继续使用,保留是为了说明还有关卡配置表。
PveRobotConf = book.sheet_by_name(PveRobotConf_Name)
PveRobotAttrs = book.sheet_by_name(PveRobotAttrs_Name)
sh = PveRobotAttrs
# 第一行是字段名,通过字段名定位列比写死列号更适合配置表迭代。
titles = sh.row_values(0)
# print("numbers of worksheets: {0}".format(book.nsheets))
# print("worksheet names: {0}".format(book.sheet_names()))
# print("{0} {1} {2}".format(sh.name, sh.nrows, sh.ncols))
# 从第 5 行开始读取,跳过表头和说明行;两列分别是角色 ID 与职业类型。
data = [sh.col_values(titles.index(PveRobotAttrs_ID_Name),4),
sh.col_values(titles.index(PveRobotAttrs_JOB_Name),4)]
return data
  • 生成与筛选

逻辑相对简单,一些对数据切片和匹配的细节就不再展开说了。

def process_each_level(begin, end, data, result, level):
# 输入 data 形如 [[id...], [job...]],先转置为 [[id, job], ...]。
data = map(list, zip(*data))
# Excel 读出的数字可能是浮点或字符串,统一转成 int 便于比较和输出。
data = map(lambda l : map(int, l), data)
# 按 ROLE_NUM 分组,并只取 [begin, end) 范围内的角色槽位。
data = map(lambda x : data[x::ROLE_NUM], range(begin, end))
# 再次转置,把同一关卡可选的角色放到一起,便于组合枚举。
data = map(list, zip(*data))
# 如果只需要扁平列表,可以用 chain 展开;这里保留分组以便 combinations 使用。
# data = itertools.chain(*data)
# 从候选角色组中选择 4 组,再在每组内部做笛卡尔积生成具体角色配置。
for each_select in itertools.combinations(data, 4):
process_each_select(each_select, result, level)
return result
def process_each_select(each_select, result, level):
# product(*each_select) 会枚举每组候选中的所有组合。
for each_result in itertools.product(*each_select):
# each_result 的元素形如 [id, role],分离 ID 和职业用于输出与校验。
ids = map(lambda l: l[0], each_result)
roles = map(lambda l: l[1], each_result)
# 职业不能重复;set 去重后长度不变,说明四个角色职业互不相同。
if (len(set(roles)) == len(roles)):
# 输出格式:[自增ID, 关卡ID, 预留字段, 角色ID...]
each_result = list(itertools.chain(*[[len(result) + 1], [level], [""], ids]))
result.append(each_result)
  • 输出

因为有大量关卡,这里我们修改config就可以配置关卡与角色起终止一次性生成全部配置。

def process_all():
result = []
data = read_data()
# config 的每一项表示:[关卡ID, [角色槽位起点, 角色槽位终点)]。
# 通过改这里,可以一次性为多个关卡批量生成配置。
config = [
[10000, [0,2]],]
for line in config:
# line[1] 控制槽位范围,line[0] 写入结果中的关卡 ID。
process_each_level(line[1][0],line[1][1], data, result, line[0]);
return result
if __name__ == "__main__":
result = process_all()
# Python 2 时代 csv 常用二进制模式;Python 3 可改为 open(..., 'w', newline='')。
with open(OUTPUT_PATH, 'wb') as f:
csv.writer(f).writerows(result)

唔,中间的删选部分或许可以再抽象一层,但是也没必要,毕竟是能work就好,生成后大概几十万行的配置,如果手工的话感觉我一辈子就没了。