当我们处理数据文件的,一般来说,写文件比读取文件内容来的容易。因为我们可以很好的控制文件的写操作,而从文件读取数据常常碰到不可预知的情况。一个健壮的程序不仅应该可以读取存有正确格式的数据还应该能够处理坏文件(译者注:对数据内容和格式进行校验,对异常情况能够做出恰当处理)。正因为如此,实现一个健壮的读取数据文件的程序是很困难的。
正如我们在
Section 10.1
(译者:第
10
章
Complete Examples
)中看到的例子,文件格式可以通过使用
Lua
中的
table
构造器来描述。我们只需要在写数据的稍微做一些做一点额外的工作,读取数据将变得容易很多。方法是:将我们的数据文件内容作为
Lua
代码写到
Lua
程序中去。通过使用
table
构造器,这些存放在
Lua
代码中的数据可以像其他普通的文件一样看起来引人注目。
为了更清楚地描述问题,下面我们看看例子。如果我们的数据是预先确定的格式,比如
CSV
(逗号分割值),我们几乎没得选择。(在第
20
章,我们介绍如何在
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) count = 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
。
12.1 序列化
我们经常需要序列化一些数据,为了将数据转换为字节流或者字符流,这样我们就可以保存到文件或者通过网络发送出去。我们可以在
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
12.1.1保存不带循环的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/"",
}
我们可以通过测试每一种情况,看是否需要方括号,另外,我们将这个问题留作一个练习给大家。
12.1.2保存带有循环的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)
saved = 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
a.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
给予你权力,由你决定如何实现。