第12章 数据文件与持久化

12 数据文件与持久化
当我们处理数据文件的,一般来说,写文件比读取文件内容来的容易。因为我们可以很好的控制文件的写操作,而从文件读取数据常常碰到不可预知的情况。一个健壮的程序不仅应该可以读取存有正确格式的数据还应该能够处理坏文件(译者注:对数据内容和格式进行校验,对异常情况能够做出恰当处理)。正因为如此,实现一个健壮的读取数据文件的程序是很困难的。
正如我们在 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 给予你权力,由你决定如何实现。
 
 
 

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值