文章来自:
LUA脚本语言之数据文件与持久化是本文要介绍的内容,当我们处理数据文件的,一般来说,写文件比读取文件内容来的容易。因为我们可以很好的控制文件的写操作,而从文件读取数据常常碰到不可预知的情况。
一个健壮的程序不仅应该可以读取存有正确格式的数据还应该能够处理坏文件(译者注:对数据内容和格式进行校验,对异常情况能够做出恰当处理)。正因为如此,实现一 个健壮的读取数据文件的程序是很困难的。
文件格式可以通过使用Lua中的table构造器来描述,我们只需要在写数据的稍微做一些做一点额外的工作,读取数据将变得容易很多。方法是:将我们的数据文件内容作为Lua代码写到Lua程序中去。通过使用table构造器,这些存放在Lua代码中的数据可以像其他普通的文件一样看起来引人注目。
为了更清楚地描述问题,下面我们看看例子。如果我们的数据是预先确定的格式,比如CSV(逗号分割值),我们几乎没得选择。但是如果我们打算创建一个文件为了将来使用,除了CSV,我们可以使用Lua构造器来我们表述我们数据,这种情况下,我们将每一个数据记录描述为一个Lua构造器。将下面的代码
Donald E. Knuth,Literate Programming,CSLI,1992 Jon Bentley,More Programming Pearls,Addison-Wesley,1990 写成 Entry{"Donald E. Knuth", "Literate Programming", "CSLI", 1992} Entry{"Jon Bentley", "More Programming Pearls", "Addison-Wesley", 1990}
记住Entry{...}与Entry({...})等价,他是一个以表作为唯一参数的函数调用。所以,前面那段数据在Lua程序中表示如上。如果要读取这个段数据,我们只需要运行我们的Lua代码。例如下面这段代码计算数据文件中记录数:
local count = 0 function Entry (b) countcount = count + 1 end dofile("data") print("number of entries: " .. count)
下面这段程序收集一个作者名列表中的名字是否在数据文件中出现,如果在文件中出现则打印出来。(作者名字是Entry的第一个域;所以,如果b是一个entry的值,b[1]则代表作者名)
local authors = {} -- a set to collect authors function Entry (b) authors[b[1]] = true end dofile("data") for name in pairs(authors) do print(name) end
注意,在这些程序段中使用事件驱动的方法:Entry函数作为回调函数,dofile处理数据文件中的每一记录都回调用它。当数据文件的大小不是太大的情况下,我们可以使用name-value对来描述数据:
Entry{ author = "Donald E. Knuth", title = "Literate Programming", publisher = "CSLI", year = 1992 } Entry{ author = "Jon Bentley", title = "More Programming Pearls", publisher = "Addison-Wesley", year = 1990 }
(如果这种格式让你想起BibTeX,这并不奇怪。Lua中构造器正是根据来自BibTeX的灵感实现的)这种格式我们称之为自描述数据格式,因为每一个数据段都根据他的意思简短的描述为一种数据格式。相对CSV和其他紧缩格式,自描述数据格式更容易阅读和理解,当需要修改的时候可以容易的手工编辑,而且不需要改动数据文件。例如,如果我们想增加一个域,只需要对读取程序稍作修改即可,当指定的域不存在时,也可以赋予默认值。使用name-value对描述的情况下,上面收集作者名的代码可以改写为:
local authors = {} -- a set to collect authors function Entry (b) authors[b.author] = true end dofile("data") for name in pairs(authors) do print(name) end
现在,记录域的顺序无关紧要了,甚至某些记录即使不存在author这个域,我们也只需要稍微改动一下代码即可:
function Entry (b) if b.author then authors[b.author] = true end end
Lua不仅运行速度快,编译速度也快。例如,上面这段搜集作者名的代码处理一个2MB的数据文件时间不会超过1秒。另外,这不是偶然的,数据描述是Lua的主要应用之一,从Lua发明以来,我们花了很多心血使他能够更快的编译和运行大的chunks。
序列化
我们经常需要序列化一些数据,为了将数据转换为字节流或者字符流,这样我们就可以保存到文件或者通过网络发送出去。我们可以在Lua代码中描述序列化的数据,在这种方式下,我们运行读取程序即可从代码中构造出保存的值。
通常,我们使用这样的方式varname = <exp>来保存一个全局变量的值。varname部分比较容易理解,下面我们来看看如何写一个产生值的代码。对于一个数值来说:
function serialize (o) if type(o) == "number" then io.write(o) else ... end
对于字符串值而言,原始的写法应该是:
if type(o) == "string" then io.write("'", o, "'")
然而,如果字符串包含特殊字符(比如引号或者换行符),产生的代码将不是有效的Lua程序。这时候你可能用下面方法解决特殊字符的问题:
if type(o) == "string" then io.write("[[", o, "]]")
千万不要这样做!双引号是针对手写的字符串的而不是针对自动产生的字符串。如果有人恶意的引导你的程序去使用" ]]..os.execute('rm *')..[[ "这样的方式去保存某些东西(比如它可能提供字符串作为地址)你最终的chunk将是这个样子:
varname = [[ ]]..os.execute('rm *')..[[ ]]
如果你load这个数据,运行结果可想而知的。为了以安全的方式引用任意的字符串,string标准库提供了格式化函数专门提供"%q"选项。它可以使用双引号表示字符串并且可以正确的处理包含引号和换行等特殊字符的字符串。这样一来,我们的序列化函数可以写为:
function serialize (o) if type(o) == "number" then io.write(o) elseif type(o) == "string" then io.write(string.format("%q", o)) else ... end
保存不带循环的table
我们下一个艰巨的任务是保存表。根据表的结构不同,采取的方法也有很多。没有一种单一的算法对所有情况都能很好地解决问题。简单的表不仅需要简单的算法而且输出文件也需要看起来美观。
我们第一次尝试如下:
function serialize (o) if type(o) == "number" then io.write(o) elseif type(o) == "string" then io.write(string.format("%q", o)) elseif type(o) == "table" then io.write("{\n") for k,v in pairs(o) do io.write(" ", k, " = ") serialize(v) io.write(",\n") end io.write("}\n") else error("cannot serialize a " .. type(o)) end end
尽管代码很简单,但很好地解决了问题。只要表结构是一个树型结构(也就是说,没有共享的子表并且没有循环),上面代码甚至可以处理嵌套表(表中表)。对于所进不整齐的表我们可以少作改进使结果更美观,这可以作为一个练习尝试一下。(提示:增加一个参数表示缩进的字符串,来进行序列化)。
前面的函数假定表中出现的所有关键字都是合法的标示符。如果表中有不符合Lua语法的数字关键字或者字符串关键字,上面的代码将碰到麻烦。一个简单的解决这个难题的方法是将:
io.write(" ", k, " = ") 改为 io.write(" [") serialize(k) io.write("] = ")
这样一来,我们改善了我们的函数的健壮性,比较一下两次的结果:
result of serialize{a=12, b='Lua', key='another "one"'}
第一个版本
{ a = 12, b = "Lua", key = "another \"one\"", }
第二个版本
{ ["a"] = 12, ["b"] = "Lua", ["key"] = "another \"one\"", }
我们可以通过测试每一种情况,看是否需要方括号,另外,我们将这个问题留作一个练习给大家。
保存带有循环的table
针对普通拓扑概念上的带有循环表和共享子表的table,我们需要另外一种不同的方法来处理。构造器不能很好地解决这种情况,我们不使用。为了表示循环我们需要将表名记录下来,下面我们的函数有两个参数:table和对应的名字。另外,我们还必须记录已经保存过的table以防止由于循环而被重复保存。我们使用一个额外的table来记录保存过的表的轨迹,这个表的下表索引为table,而值为对应的表名。
我们做一个限制:要保存的table只有一个字符串或者数字关键字。下面的这个函数序列化基本类型并返回结果。
function basicSerialize (o) if type(o) == "number" then return tostring(o) else -- assume it is a string return string.format("%q", o) end end
关键内容在接下来的这个函数,saved这个参数是上面提到的记录已经保存的表的踪迹的table。
function save (name, value, saved) savedsaved = saved or {} -- initial value io.write(name, " = ") if type(value) == "number" or type(value) == "string" then io.write(basicSerialize(value), "\n") elseif type(value) == "table" then if saved[value] then -- value already saved? -- use its previous name io.write(saved[value], "\n") else saved[value] = name -- save name for next time io.write("{}\n") -- create a new table for k,v in pairs(value) do -- save its fields local fieldname = string.format("%s[%s]", name, basicSerialize(k)) save(fieldname, v, saved) end end else error("cannot save a " .. type(value)) end end
举个例子:
我们将要保存的table为:
a = {x=1, y=2; {3,4,5}} a[2] = a -- cycle aa.z = a[1] -- shared sub-table
调用save('a', a)之后结果为:
a = {} a[1] = {} a[1][1] = 3 a[1][2] = 4 a[1][3] = 5 a[2] = a a["y"] = 2 a["x"] = 1 a["z"] = a[1]
(实际的顺序可能有所变化,它依赖于table遍历的顺序,不过,这个算法保证了一个新的定义中需要的前面的节点都已经被定义过)
如果我们想保存带有共享部分的表,我们可以使用同样table的saved参数调用save函数,例如我们创建下面两个表:
a = { {"one", "two"}, 3} b = {k = a[1]}
保存它们:
save('a', a) save('b', b)
结果将分别包含相同部分:
a = {} a[1] = {} a[1][1] = "one" a[1][2] = "two" a[2] = 3 b = {} b["k"] = {} b["k"][1] = "one" b["k"][2] = "two"
然而如果我们使用同一个saved表来调用save函数:
local t = {} save('a', a, t) save('b', b, t)
结果将共享相同部分:
a = {} a[1] = {} a[1][1] = "one" a[1][2] = "two" a[2] = 3 b = {} b["k"] = a[1]
上面这种方法是Lua中常用的方法,当然也有其他一些方法可以解决问题。比如,我们可以不使用全局变量名来保存,即使用封包,用chunk构造一个local值然后返回之;通过构造一张表,每张表名与其对应的函数对应起来等。Lua给予你权力,由你决定如何实现。
小结:详解LUA脚本语言之数据文件与持久化的内容介绍完了,希望通过本文的学习能对你有所帮助!