Lua表存储优化

6.5k 词

用Python脚本实现对 Lua表的压缩



问题

在研发过程中,通常会定义一些Excel表格,规定行列值让策划填写,然后,转成lua的table文件,直接使用。

但是,随着研发进行,项目迭代,表格将越来越大。

如果表格中存在大量重复数据,或者表格中很多列数值重复,则可以通过数据压缩给表减减肥。

解决

利用python实现lua表的数据压缩

  • excel表内存在大量 同列不同行 内容一致
  • excel表内存在大量 复合型 单元格内容一致

具体代码,均在github中: ltree98’s github -> CompressLua




参照方案

这是我在网上看到的文章:

Lua配置表存储优化方案

总结的来说,就是

  • 利用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


    local tableName = {
    ...
    }
    return tableName
  • 默认表:未来的元表,存储Excel中每列最频繁的元素

  • 重复表:Excel表中各单元格重复的 复合型元素(array/table)


流程

  1. 遍历表,进行统计

    • 统计Excel表中各列元素出现次数

    • 统计Excel表中 单元格复合型元素 出现次数

  2. 构造 重复表

    • 筛选 单元格复合型元素 次数大于1的元素,构建重复表
  3. 构造 默认表

    • 根据各列最频繁元素,构造默认表
  4. 整理 Excel表

    • 根据默认表,将重复的字段忽略
  5. 输出 各表

    • 根据 重复表,替换并输出 重复表
    • 根据 重复表,替换并输出 Excel表
    • 根据 重复表,替换并输出 默认表
  6. 设置 元表 及 只读属性


关键代码

最新代码请移步 lt-tree的github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
###################################################################
##
## tools
##

def (dict_temp):
"""Count dictionary's deep

Args:
dict_temp: dict

Returns:
int : deep
"""

deep = 0
for item in dict_temp.values():
if isinstance(item, dict):
temp_deep = count_dict_deep(item)
if temp_deep > deep:
deep = temp_deep

return deep+1

def calc_weight(obj1):
"""Calculate obj's weight

Args:
obj1: tuple[dict's string, dict's frequency]

Returns:
int : weight
"""

dict1 = eval(obj1[0])
times1 = obj1[1]

deep1 = count_dict_deep(dict1)
ans = deep1 + 1/times1

return ans

def count_table_frequency(unit_dict, dict_frequency):
"""Count table frequency

Count table frequency and record as {table string: times}

Args:
unit_dict: dict, need analyse data
dict_frequency: dict, the record's set
"""

unit_str = str(unit_dict)
if unit_str in dict_frequency:
dict_frequency[unit_str] = dict_frequency[unit_str] + 1
else:
dict_frequency[unit_str] = 1

# traversing sub dict
for item in unit_dict.values():
if isinstance(item, dict):
count_table_frequency(item, dict_frequency)


def count_table_value_frequency(key, value, item_frequency):
"""Count table value frequency

Count every excel column element appear times.
Record as {
key1 : {element1 : times, element2: times, ...}
key2 : {element1 : times, element2: times, ...}
...
}

Args:
key: string
value: string or dict
item_frequency: dict, the record's set
"""

if isinstance(value, dict):
value = str(value)

if key in item_frequency.keys():
if value in item_frequency[key].keys():
item_frequency[key][value] = item_frequency[key][value] + 1
else:
item_frequency[key][value] = 1
else:
item_frequency[key] = {}
item_frequency[key][value] = 1


def traverse_table(excel_dict, dict_frequency, item_frequency):
"""Traverse table.

Analyse lua table.

Args:
excel_dict: dict
dict_frequency: dict
item_frequency: dict
"""

for key in sorted(excel_dict):
if isinstance(excel_dict[key], dict):
count_table_frequency(excel_dict[key], dict_frequency)

for k, v in excel_dict[key].items():
count_table_value_frequency(k, v, item_frequency)


def check_repeat_dict(item_dict, repeat_dict):
"""Check repeat dict

Check repeat dict and return the repeat index, if not exist in repeat dict return -1.

Args:
item_dict: dict
repeat_dict: dict

Returns:
int
"""

dict_str = str(item_dict)
if dict_str in repeat_dict.keys():
return repeat_dict[dict_str]
else:
return -1

def replace_repeat_dict(item_dict, repeat_dict, cur_index = -1):
"""Replace repeat dict

Check if element exist in repeat dict and replace by designation string.

Args:
item_dict: dict
repeat_dict: dict
"""

cur_index = -1

for key, value in item_dict.items():
if isinstance(value, dict):
index = check_repeat_dict(value, repeat_dict)
if index != -1 and (index < cur_index or cur_index == -1):
if index > 190:
item_dict[key] = REPEAT_KEY_PREFIX + '[' + str(index - LOCAL_TABLE_MAX) + ']'
else:
item_dict[key] = REPEAT_KEY_PREFIX + str(index)
else:
replace_repeat_dict(value, repeat_dict, cur_index)



def output_file(table_name, file_path, repeat_dict, final_dict, default_dict):
"""Output file

Args:
table_name: string
file_path: path
repeat_dict: dict
final_dict: dict
default_dict: dict
"""

file_handler = codecs.open(file_path, 'a', encoding='utf-8')

# output repeat dict
for dictStr, index in sorted(repeat_dict.items(), key=lambda item:item[1]):

# replace repeat item by repeat_dict
repeat_dict_item = eval(dictStr)
replace_repeat_dict(repeat_dict_item, repeat_dict, index)

if index <= LOCAL_TABLE_MAX:
# file_handler.write("local %s = {n" % (REPEAT_KEY_PREFIX + str(index)))
file_handler.write("local %s = {n" % (REPEAT_KEY_PREFIX + str(index)))
convert.convert_dict_lua_file(file_handler, repeat_dict_item, 1)
file_handler.write("}n")
else:
if index == (LOCAL_TABLE_MAX + 1):
file_handler.write("nlocal __rt = createtable and createtable(%d, 0) or {}n" % (len(repeat_dict)-LOCAL_TABLE_MAX))

file_handler.write("__rt[%d] = {n" % (index - LOCAL_TABLE_MAX))
convert.convert_dict_lua_file(file_handler, repeat_dict_item, 1)
file_handler.write("}n")

# output final dict
replace_repeat_dict(final_dict, repeat_dict)
file_handler.write("nlocal %s = {n" % (table_name))
convert.convert_dict_lua_file(file_handler, final_dict, 1)
file_handler.write("}n")

# output default dict