我们团队的技术栈是: Lua + Python
, 前端采用 Lua 写项目, Python 做工具支持, 而后端则采用纯 Python 开发, 所以会经常遇到 Python 和 Lua 数据的转化.
目前业内采用的方法大致有两种:
- 用 Python 写一个 Lua 的语法解析, 用 Lua 写一个 Pyhton 的语法解析, 这样就可以互相转化了.
- 使用一个大家都支持的数据结构做中转, 如: Json 或 Yaml.
第一种方案比较复杂, Python 解析 Lua 倒是有 slpp 这样的项目, Lua 解析 Python 的则是没有, 因此大家更多的是采用第二种方案.
第二种方案中 json 其实是一个很不错的解决方案, 性能也比 Lua 快一些, 但是 json 没有办法存储 number 类型的 key, 这一点在数据结构的设计上很蛋疼, 可能会让你的代码充斥着 tostring
, tonumber
这样的代码.
经过观察, json 和 Lua 这两种文件格式其实是十分像的, 那么我是否只要魔改下 python 的 json 库, 使之支持 number 做 key, 然后保存为 Lua 文件 ?
开撸
1. 准备工作
首先我们找到 json 库的源码 https://github.com/python/cpython/blob/master/Lib/json, 这个目录中有 4 个文件, 我们把 encoder.py
下载下来, 保存为 lua.py
.
但是只有这一个文件是没有办法直接运行的, 我们再从 encoder.py
同级的 __init__.py
中复制 dumps
函数到 lua.py
的末尾.
1 | def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, |
我们在 lua.py
同级目录新建一个 test.py
做测试, 这里我们把一个python的数据保存为 test.lua
文件:
1 | import lua |
完成后直接终端运行 python test.py
就可以看到一个新生成了一个 lua 文件, 内容如下:
1 | { |
我们第一步的工作就搞定了.
2. 魔改 lua.py
其实上一步生成的文件内容并不是 lua 格式的, 其实还是 json 格式, 只是我们将后缀改为 lua 了而已. 这个格式和真正的 lua 想比有两处不对:
- Lua 的数组使用
{}
表示. - Lua 使用
=
做 key,value 的分隔. - Lua string 类型的 key, 不用加引号, 如果要加引号的话, 需要同时加一对方括号.
第一个问题很好解决, 我们搜索 '[
, 可以找到 _iterencode_list
这个函数, 大概看一眼就知道怎么改了:
1 | def _iterencode_list(lst, _current_indent_level): |
我们修改完, 运行 python test.py
看下效果:
1 | { |
是不是已经有点意思了? 我们再接再厉, 找找第二个问题如何解决. 我们搜索 ':
可以找到这行代码:
1 | key_separator = ': ' |
将 ': '
修改为 ' = '
就可以啦, 我们看下结果:
1 | { |
那么第三个问题该如何入手呢? 给童鞋们三秒钟的思考时间.
3s
2s
1s
揭晓答案, 既然 key_separator
是分隔 key, value 的, 我们找找哪里用到 key_separator 是不是发现点什么呢 ? 很快我们能定位到 _iterencode_dict
函数的这行代码:
1 | yield _encoder(key) |
这个 _encoder(key)
会不会对应 key 的生成呢? 我们修改下试试, 将这句话替换为:
1 | yield '[' + _encoder(key) + ']' |
运行, 结果如下:
1 | { |
哎呀, 我这是搞定了吗? 骚年, 恭喜你, 你已经完成了 99% 的工作了, 这段代码的最前方还少一个 return
, 我们可以很轻易的加上. 我通过修改 test.py
实现了这个功能:
1 | f.write(lua.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)) |
结果:
1 | return { |
3. 更加 lua 化
上面生成的 test.lua
其实很不 Lua, 大家不会写出 ["a"] = 1
这种类型的语句, 而是直接 a = 1
.
这个问题发生在了上面的第三步上, 我们偷懒直接写出了这样的代码:
1 | yield '[' + _encoder(key) + ']' |
大家不妨看看 _encoder
这个函数, 它会把任意类型转化为字符串, 两端加上 "
. 那么 Lua 在什么情况下才会 ["x"]
这种类型的 key 呢?
- 关键字, 如:
require
,false
等, 我们可以在这里找到所有的关键字. - 以数字开头的字符串.
- 数字, 包括
int
,lang
,float
. - 含有奇怪字符的字符串, 如:
-
,*
等, 非奇怪字符只有:[a-zA-Z_0-9]
.
知道了规则, 我们就很好办了, 写一个解析函数就好了嘛:
1 | key_filter = re.compile("[^a-zA-Z_0-9]") |
然后把 yield '[' + _encoder(key) + ']'
替换为 yield format_key(key)
就搞定了. 我们看下生成的结果:
1 | return { |
好像不太对嘛, 骗纸, 数字的 key 还是有问题的. 不要慌, 我们顺着 format_key
调用的地方向上找找, 发现问题了吧:
1 | elif isinstance(key, float): |
可以看到这里已经对 float
, int
, lang
类型做过转化了, 我们把 elif 中的内容换成 pass
再试下:
1 | return { |
哈哈, 搞定了! 我太棒了! 咦, 纳尼, Are you kidding me ? 为什么只有 2
是 ok 的, "4"
是怎么回事嘛 ? 仔细检查了一番代码, 有做了一个额外的修改, 还是没有发现原因.
当我把目光转向我们的测试代码的时候, 我看到的答案:
1 | data = { |
我们的 "4"
本身就是 string 类型的, 这样更加说明了我们代码的正确性.
到此为止, 我们的魔改之旅就结束了, 我把最终版的代码放到了 Gist 上, 大家如果有兴趣的话可以看一下.
附录:
一个 24W 行的 json 和 Lua 读取时间:
1 | time1 = os.clock() |
结果如下:
1 | time lua: 0.278526 |