用Python脚本实现对 Lua表的压缩
前言
问题
在研发过程中,通常会定义一些Excel表格,规定行列值让策划填写,然后,转成lua的table文件,直接使用。
但是,随着研发进行,项目迭代,表格将越来越大。
如果表格中存在大量重复数据,或者表格中很多列数值重复,则可以通过数据压缩给表减减肥。
解决
利用python实现lua表的数据压缩
- excel表内存在大量 同列不同行 内容一致
- excel表内存在大量 复合型 单元格内容一致
具体代码,均在github中: ltree98’s github -> CompressLua
参照方案
这是我在网上看到的文章:
总结的来说,就是
- 利用lua的元表机制。如果在table中取的值不存在,会去它的元表中查找(如果元表存在)。
- 将重复的table,提取出来,将所有使用的地方引用过去。
问题
在参照方案的文章中,也提供了解决方法,但是在使用过程中遇见一些问题。
对同一文件压缩后文件MD5可能不一致
对同一个lua文件,进行减肥,在减肥后,虽然最终体重一样,但是可能这次瘦了肚子,下次瘦了腿。
主要原因就是参照方案使用lua处理,对同一个table遍历顺序是不稳定的,即table中存在 A B C 元素,可能这次遍历顺序是 A-B-C,下次遍历顺序可能就是 B-A-C;稳定性得不到保证。
lua的官网对于table的遍历描述一直是:
The order in which the indices are enumerated is not specified, even for numeric indices. (To traverse a table in numeric order, use a numerical for or the
ipairs
function.)
就是 索引在遍历过程中的次序是不固定的,即使是数字索引也不固定。
因为,项目中使用热更机制是比较两个文件的MD5,决定是否更新该文件。
所以,这个问题就显得很严重,每次热更要更新所有的表。当然,这个问题也可以通过做备份等方式来弥补,但是毕竟治标不治本。
但是用不同的lua版本发现:
使用 lua5.1 版本 生成的压缩文件是一致的
使用 lua5.3版本 生成的压缩文件是不一致的
复杂度高
虽然是现成的,但是也没法直接拿来用;
还是要根据项目现状进行修改。
在整合过程中,发现逻辑比较复杂,而且工具集与我们现有的python不符。(如果用其他工具集,又要去配置相应环境等,比较麻烦)。
修改方案
简介
目的
- 解决 Excel表 内存在大量 同列不同行 内容一致
- 解决 Excel表内存在大量 复合型单元格 内容一致
设计
- 对于重复table,只存一份,其他引用过去
- 设计一个基础元表,存储Excel每列 最频繁的值;其他表缺省 最频繁的值
- 设置 只读属性
注意
- Lua作用域内存放最大的local变量个数为 200个,超过的需要放入临时数组
- 输出到lua文件,key值不可存在特殊字符($、- 等)
缺点
- Lua表的可读性变差,需要跳转获取最终值
- 使用元表实现,若后续处理(比如 加密,修改操作 等)也存在使用元表,增加处理的复杂度
方案
名词解释
Excel表:Excel转换成Lua的表,一般结构为
1
2
3
4
5
6-- tableName.lua
local tableName = {
...
}
return tableName默认表:未来的元表,存储Excel中每列最频繁的元素
重复表:Excel表中各单元格重复的 复合型元素(array/table)
流程
遍历表,进行统计
统计Excel表中各列元素出现次数
统计Excel表中 单元格复合型元素 出现次数
构造 重复表
- 筛选 单元格复合型元素 次数大于1的元素,构建重复表
构造 默认表
- 根据各列最频繁元素,构造默认表
整理 Excel表
- 根据默认表,将重复的字段忽略
输出 各表
- 根据 重复表,替换并输出 重复表
- 根据 重复表,替换并输出 Excel表
- 根据 重复表,替换并输出 默认表
设置 元表 及 只读属性
关键代码
最新代码请移步 lt-tree的github
1 | ################################################################### |
文件结构
- six.py:专门用于兼容 Python2 和 Python3 的库
- slpp.py:用来将 lua的table结构 转换成 python dictionary结构
- BEBase.py:基础模块,目前包含 转换、格式化输出等
- CompressLua.py :压缩Lua工具
其他
为什么 lua5.1 有序, lua5.3 无序
最开始使用lua5.3来执行 参照方案,所以出现了压缩后的文件不一致问题;
一开始也没有想是lua版本的问题,只是单纯觉得用lua实现太不方便了,就直接用python重写了。
后来在实现完python版本,然后进行整理的时候,
发现lua5.1版本其实还是固定顺序的(虽然官方文档写的不保证),
但是lua5.3版本的确是无序,而且这个无序是载入无序,
也就是说,对同一个table,载入时会安排好顺序,后面遍历就固定顺序了,但是下次载入可能就换了个顺序。
为什么lua5.3会载入无序呢?
lua中table的 for pairs 遍历是通过next来实现的,next的实现在不同lua版本没有变化
1 | int luaH_next (lua_State *L, Table *t, StkId key) { |
都是先查数组部分,再查哈希部分。
所以,key值的设置就是关键。
lua关于table中加入新key值解释是:
/
inserts a new key into a hash table; first, check whether key’s main position is free. If not, check whether colliding node is in its main
position or not: if it is not, move colliding node to an empty place and put new key in its main position; otherwise (colliding node is in its main
** position), new key goes to an empty position. /
向哈希表插入新key值,
- 先检查key的 mainposition(哈希值) 是否空着
- 是:key值放在 mainposition
- 否:查看占据位置的 值 的 mainposition 是否是该位置
- 是:找到 空位置,然后把 key值放在该位置
- 否:将占据位置的值 移动到空位置,然后将key值放在 该位置
核心就是,
当发生哈希碰撞的时候,会在碰撞位置 物理向上查找到空位置,然后作为 后加入值 的位置。
因此,出现发生碰撞的时候,需要先去判断 占据位置的值 的哈希值是否是 该位置。
由于,对同一个 哈希值 的不同值,是有链关系的,所以,后来的兄弟被移到别的位置了,还是能被找到,并不会因为后来兄弟把它扔别的地方就丢了。
最后,当没有空位置的时候,就要进行 rehash操作,这个设计到lua表的具体实现,等以后有空再整理一下吧。
mainposition的值是怎么来的呢?
这里先只讨论string类型的值,
对于 lua5.1 来说:
1 | TString *luaS_newlstr (lua_State *L, const char *str, size_t l) { |
此处,h的取值即为该string的hash key,可以发现它是将 string长度作为种子,计算出来的,所以 lua5.1 是固定顺序的。
对于 lua5.3 来说:
1 | TString *luaS_createlngstrobj (lua_State *L, size_t l) { |
此处,h取值是一个随机种子,在创建的时候随机生成的:
1 | /* |
至此,破案。
关于 repeat_dict 的嵌套问题(v0.0.2更新)
问题
最终输出的lua会存在以下结构:
1 | local __rt1 = { |
这样在lua使用时,由于定义__rt1时,__rt2未被声明定义,所以输出出来的结果是:
1 | __rt1 = { |
这是由于,repeat_dict只是单纯根据出现频繁次数排序的,所以可能存在上面的去引用下面的情况。
解决
方案一
这种方案是比较正规的方法,
将repeat_dict内所有序号都看作一个节点,然后通过引用关系相连,可以生成多条链结构。
根据链的长度进行遍历,共同维护将要输出的最终链结构
每遍历一个节点,都先去最终结构遍历位置关系及是否已经输出
找到合适的位置,将结点插入进去
将最终链结构按顺序赋值输出
方案二
这种方案比较投机取巧一些。
因为存在嵌套关系,那肯定是递归深度高的table嵌套了递归深度低的table。
所以先输出递归深度低的table,再输出递归深度高的table,那就不会出现这种问题。
但是,为了更好查找,同样深度的table,根据出现频率要再排序。
针对不同python版本打表不一致问题(v0.0.3更新 & v0.0.4更新)
经使用发现,不同python版本会导致,输出的表内容不一致。(暂时只试验了 Python 3.4 及 Python 3.7)
经过试验查询,发现是Python在存到字典后读取的顺序是不同的,导致sorted排序会有所差异(Python3.7没有问题,Python3.4会有所差异)。
查了相关资料发现,在python3.6开始 普通字典可以记住元素添加顺序,之前并没有,所以会导致之前的字典遍历顺序有问题。
- PS:注意,此处排序的稳定性 和 添加顺序一致 是两个概念。
- 排序的稳定性:对于待排序数列 1,1,2,3, 经历稳定排序,则不会进行交换操作,第一个位置的1 和 第二个位置的1 不会交换位置(虽然两者值相同)
- 添加顺序的一致:输入 age, sex, sex, age;如果添加顺序一致,那么 sex永远会优先遍历到第二次,从而顺序会是 {sex: 2, age: 2};如果添加顺序不一致,就可能出现 {age: 2, sex: 2}
原因查明白了,该想怎么解决了。就是对同样频率的再根据key排个序。(没有什么顺序是排序解决不了的,如果不行,多排几遍…)
基于同样的问题,v0.0.4进一步修改,字典转字符串,要先对字典排序再转,否则将出现根据存储顺序不一致问题,然后比较是否重复元素,也由原来比较字符串相同改为比较字典相同。
参考资料