« skynet 的 UDP 支持 | 返回首页 | 乐观锁和悲观锁 »

策划们离不开的 Excel

我相信至少在国内的游戏策划圈, Excel 是每天必不可少的存在。倒不是因为要用它制作数值表格,一切文档最终都一定是用 Excel 写的。但作为一个程序员,我相当的痛恨 Excel 文件,就好像我当初痛恨 word 一样。只有几个字就不要保存成 doc 文件啦,可现在已经没有人用 word 了,大家全转去 Excel 了。如果有可能,策划一定愿意在单元格里写脚本的,这样可以将重点标红。

提取 Excel 中的文字信息并不复杂,但真正的麻烦在于 Excel 文件对版本管理工具是极不友好的。甚至你打开一次 Excel 文件再保存关闭,也会生成一个完全不同的新版本。这是因为,文件中记录了最后修改的时间(是的,Excel 不信任文件系统里的时间);还有激活的单元格是哪一个。在这种环境下,多人协作的版本控制工具用起来绝对是一个悲剧。

我大概花了一周时间来试图解决一系列问题。结果不算成功,也不算失败。这里记录一下上周踩过的坑。

问题源于我们的项目中,策划把一切他们能生产的东西都记在了诸多的 excel 表格里。当然,和上世纪的程序员一样,大家都尽量自己维护自己的那块文件,所以即使在版本管理工具下,也基本没有冲突。但是总有那么 1% 的机会,几个人会修改同一张表格的,尤其在项目压力大时,往往实现功能的程序也会打开表格对里面的数据做一些修改。在版本控制工具下,冲突就在所难免了。尤其是我们刚刚让策划从 svn 迁移到 git 下,git 的工作流的复杂性很容易让策划的脑子不够用了(实际上受 Excel 文件格式限制,他们也只需要一个版本备份工具,其它本来就是多余的)。我开始动念头来解决问题。

首先,xlsx 文件其实是一个标准 zip 压缩包,里面打包了一系列 xml 文件。如果仅仅是需要一个文本格式,那么只需要把包解开,用一种非压缩的形式重新打包即可。

对于一些嵌入的图片,只需要用 base64 编码。由于嵌入表格的图片多半不会修改,所以并不会造成版本间的差异。

一开始,我以为这项工作两小时就能搞定,事后发现,太天真了。

我写了一个 lua 的小程序,可以读出 zip 包里的文件,对文件名排序,然后按文件名/内容的次序依次把文件连在一起形成一个大文本文件(其中的2进制内容使用 base64 编码)。这样处理后,xlsx 文件基本就是一个文本文件了。为了对版本管理工具友好,我对 xml 里的标签后增加了适当的分行。这样处理以后,版本管理工具基本能识别出表格数据每个版本的差异。

第2步,可以动手消除一些对版本有影响却对我们没有意义的数据段。比如文件的最后修改时间、激活的单元格等。这样、如果打开一个 excel 文件,保存后就不会产生差异。

那么,这是一个新的文件格式。怎么让 Excel (或 wps 等兼容产品)打开它编辑呢?

虽然第一反应是给 excel 写一个插件。但我知道拿不是一两个小时可以搞定的。所以我选择了一条弯路。写了一个脚本,可以生成一个临时目录/文件,在用户想打开一个自定义格式文件时,先转换为标准的 xlsx 临时文件,让关联的软件(excel 或 wps 等)编辑它。我们可以监控这个文件的变更时间,来即使把临时文件转换回去。当这个临时文件可写时,就表示已经停止编辑这个文件了(excel 对打开的文件有文件锁定)。这时,可以删除临时文件。

让自定义文件格式关联到这个脚本(我用 lua 编写的十多行程序),策划就可以直接双击自定义格式文件编辑了。


我天真的以为这可以解决大部分问题。但一试用就发现了问题。

策划很容易在表格中嵌入 ole 对象,比如他们最喜欢的 visio 绘图。对于 Ole 对象,是以2进制对象形式存在文件里的。但是、visio 这种 COM 对象序列化后有同样的问题:COM 对象其实也是一个包,而包里同样有时间戳。这同样会导致没有修改过的对象每次持久化的结果是不同的。

解决这个问题并不复杂。我注意到 7zip 是可以打开 COM 对象文件的。在网上下载 7zip 的源代码,很容易就写出一个 COM 对象文件的解码程序(大约几十行 C 代码)。我们要做的仅仅是把包里的文件时间抹掉即可。


如果进行到这里就停住,我就不会认为我在浪费时间。起码我解决了一个问题:没有编辑的 excel 文件无论保存多少次都是无差异的。文件格式变成了文本,大致上可(被程序员)读了。

可我贪心的想把问题解决的彻底点,设计一个自己的格式会更好一些。就是一个比 csv 格式强一些的,保留了有限的版面格式信息(包括单元格的宽高、颜色、边框等)的易于用文本编辑器编辑修改的文件。

我们可以把重要的信息都集中放在一起,不重要的格式信息放在另一部分。如果版本冲突,可以自由抛弃一个版本的格式信息而采用另一个版本的,这样不至于损坏数据本身。

csv 格式设计的很糟糕(比如那个双引号)。也不利于做版本比较合并(易于人编辑且容易合并的格式应该是一个单元格一行,而不是按行保存。且 csv 会丢失重要的公式文本。

我设计的格式大致是这样的:

[!sheet]
name:名字
[data]
A1:123
B1:234
C1:=SUM(A1,B1)
A2:Hello World
[value]
B1:357
[style]
...

在数据段保留有公式信息,把公式计算值放在 value 段。然后把版面信息全部放去 style 段。这样,即使 value 和 style 段全破坏,也可以直接用 excel 打开修复整张表格。有了这种格式,程序员会更愿意用 vi 打开文件直接编辑,他们绝对不想再安装 excel 的(尤其是对 linux 程序员)。找到想修改的单元格比 csv 文件更容易,也可以用最自然的 \ 转义。和 excel 原始格式不同,我们不把 string 保存在表格数据之外的地方(excel 文件会生成一个叫 sharedstrings 的 xml 文件保存所有的字符串,在表格数据里只做 index 引用)。且把更容易引起冲突的公式计算值隔离开(这样,不会因为有人分别修改了 A1 和 B1 而导致 C1 的计算值在两个版本中都不正确)。


当我大致实现完后,我发现我严重低估了 excel 的板式描述的复杂性。要完全复原 excel 里看到的所有版式信息,需要记录和分析很多的数据才能保持一致。全部实现完这样所需要的时间远远超过我的预期,所以我不想在这上面浪费时间了。

当然,做这件事情让我好好了解了一下 excel 文档的结构(大致浏览了 ISO-IEC-29500-1)。这里,我不想把半成品的工具开源。但可以提供一个副产品:我写了一个小程序,可以把 excel 转换为文本。这个工具可以帮助我们在命令行 grep excel 里的数据。需要这个工具的同学可以去我的 github 页面看 xlsx2txt 这个项目


另一些值得记一下的东西:

上周我考察了许多 xlsx 的读写库,有 go 版本的,python 版本的,lua 版本的。

lua 版本我找到一个叫 xlsxwriter 的东西。简单的阅读发现,性能上有许多提高的空间。学会一门语言,不在于学会它的语法,而在于熟悉这种语言下解决问题的各种惯用法。比如 lua 的字符串处理简单而强大,你需要熟悉它。

function Xmlwriter._escape_attributes(attribute)    
    attribute = string.gsub(attribute, '&', '&') 
    attribute = string.gsub(attribute, '"', '"')    
    attribute = string.gsub(attribute, '<', '&lt;')  
    attribute = string.gsub(attribute, '>', '&gt;')  
    return attribute        
end

这个函数有问题吗?

这样写会更好一些:

local escape_attrib_tbl = {
    ['&'] = '&amp;',
    ['"'] = '&quot;',
    ['<'] = '&lt;',
    ['>'] = '&gt;',
}

function Xmlwriter._escape_attributes(attribute)    
    attribute = string.gsub(attribute, '[&"<>]', escape_attrib_tbl)
    return attribute
end

如果你想知道能好多少,可以跑一下测试

Comments

vss用来解决冲突问题,这个比较方便,策划自己基本上不会出错了

多人协助,版本管理,直接用Excel Online不就好了。

如果在游戏的策划配置中,直接用 xml 作为配置格式,那应该会省事许多吧。

python有个xlrd可以方便地把excel转成csv

我在使用cocos2d-x 开发的时候,在C++调用Lua的时候,require一个文件多次执行不管用,LuaEngine::getInstance()->executeScriptFile这个函数

在svn下一般我们采用svn:needs-lock来避免冲突

我认为:这个世界上不是所有东西都必须进 git 目录的。试图让这个世界适应 git 的做法是不科学的。这个逻辑应该反过来。你应该做的是为 git 贡献非文本版本控制代码:)Good luck!

0_0 偶滴神,开头看到你试图花几个小时就想搞定个excel parser就醉了,具体原因翻下《软件随想录》第四章关于M$ Office格式内容……

excel版本控制确实是个头疼的问题。我们一直是用Google Doc或One Drive来处理,在线实时编辑,每次单独保存并提交,保证永远是最新的版本。

Excel2003开始支持了单个xml的文件格式, 可以直接用Excel打开(已基于文件内容关联)和保存, 每个单元格内容都是一行的.

哈哈,跟我一样,我当初写的是一个python读取excel的文件。为的就是能方便的做版本控制。
我那时候好,excel里面只有文件目录。就是sheet页多一些。但是后来还是出问题了。不知道为什么,打开的excel总是报格式错误。到我离职的时候也没解决这个问题。残念啊。

Post a comment

非这个主题相关的留言请到:留言本