数据持久化,序列化,反序列化,文件读写


    文章来自:http://mobile.51cto.com/iphone-286562.htm算法


LUA脚本语言之数据文件与持久化是本文要介绍的内容,当咱们处理数据文件的,通常来讲,写文件比读取文件内容来的容易。由于咱们能够很好的控制文件的写操做,而从文件读取数据经常碰到不可预知的状况。安全

一个健壮的程序不只应该能够读取存有正确格式的数据还应该可以处理坏文件(译者注:对数据内容和格式进行校验,对异常状况可以作出恰当处理)。正由于如此,实现一 个健壮的读取数据文件的程序是很困难的。网络

文件格式能够经过使用Lua中的table构造器来描述,咱们只须要在写数据的稍微作一些作一点额外的工做,读取数据将变得容易不少。方法是:将咱们的数据文件内容做为Lua代码写到Lua程序中去。经过使用table构造器,这些存放在Lua代码中的数据能够像其余普通的文件同样看起来引人注目。iphone

为了更清楚地描述问题,下面咱们看看例子。若是咱们的数据是预先肯定的格式,好比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对来描述数据:spa

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对描述的状况下,上面收集做者名的代码能够改写为:code

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这个域,咱们也只须要稍微改动一下代码便可:orm

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脚本语言之数据文件与持久化的内容介绍完了,但愿经过本文的学习能对你有所帮助!

相关文章
相关标签/搜索