詳解LUA腳本語言之?dāng)?shù)據(jù)文件與持久化
LUA腳本語言之數(shù)據(jù)文件與持久化是本文要介紹的內(nèi)容,當(dāng)我們處理數(shù)據(jù)文件的,一般來說,寫文件比讀取文件內(nèi)容來的容易。因為我們可以很好的控制文件的寫操作,而從文件讀取數(shù)據(jù)常常碰到不可預(yù)知的情況。
一個健壯的程序不僅應(yīng)該可以讀取存有正確格式的數(shù)據(jù)還應(yīng)該能夠處理壞文件(譯者注:對數(shù)據(jù)內(nèi)容和格式進(jìn)行校驗,對異常情況能夠做出恰當(dāng)處理)。正因為如此,實現(xiàn)一 個健壯的讀取數(shù)據(jù)文件的程序是很困難的。
文件格式可以通過使用Lua中的table構(gòu)造器來描述,我們只需要在寫數(shù)據(jù)的稍微做一些做一點額外的工作,讀取數(shù)據(jù)將變得容易很多。方法是:將我們的數(shù)據(jù)文件內(nèi)容作為Lua代碼寫到Lua程序中去。通過使用table構(gòu)造器,這些存放在Lua代碼中的數(shù)據(jù)可以像其他普通的文件一樣看起來引人注目。
為了更清楚地描述問題,下面我們看看例子。如果我們的數(shù)據(jù)是預(yù)先確定的格式,比如CSV(逗號分割值),我們幾乎沒得選擇。但是如果我們打算創(chuàng)建一個文件為了將來使用,除了CSV,我們可以使用Lua構(gòu)造器來我們表述我們數(shù)據(jù),這種情況下,我們將每一個數(shù)據(jù)記錄描述為一個Lua構(gòu)造器。將下面的代碼
- 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({...})等價,他是一個以表作為唯一參數(shù)的函數(shù)調(diào)用。所以,前面那段數(shù)據(jù)在Lua程序中表示如上。如果要讀取這個段數(shù)據(jù),我們只需要運行我們的Lua代碼。例如下面這段代碼計算數(shù)據(jù)文件中記錄數(shù):
- local count = 0
- function Entry (b) countcount = count + 1 end
- dofile("data")
- print("number of entries: " .. count)
下面這段程序收集一個作者名列表中的名字是否在數(shù)據(jù)文件中出現(xiàn),如果在文件中出現(xiàn)則打印出來。(作者名字是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
注意,在這些程序段中使用事件驅(qū)動的方法:Entry函數(shù)作為回調(diào)函數(shù),dofile處理數(shù)據(jù)文件中的每一記錄都回調(diào)用它。當(dāng)數(shù)據(jù)文件的大小不是太大的情況下,我們可以使用name-value對來描述數(shù)據(jù):
- 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
- }
(如果這種格式讓你想起B(yǎng)ibTeX,這并不奇怪。Lua中構(gòu)造器正是根據(jù)來自BibTeX的靈感實現(xiàn)的)這種格式我們稱之為自描述數(shù)據(jù)格式,因為每一個數(shù)據(jù)段都根據(jù)他的意思簡短的描述為一種數(shù)據(jù)格式。相對CSV和其他緊縮格式,自描述數(shù)據(jù)格式更容易閱讀和理解,當(dāng)需要修改的時候可以容易的手工編輯,而且不需要改動數(shù)據(jù)文件。例如,如果我們想增加一個域,只需要對讀取程序稍作修改即可,當(dāng)指定的域不存在時,也可以賦予默認(rèn)值。使用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
現(xiàn)在,記錄域的順序無關(guān)緊要了,甚至某些記錄即使不存在author這個域,我們也只需要稍微改動一下代碼即可:
- function Entry (b)
- if b.author then authors[b.author] = true end
- end
Lua不僅運行速度快,編譯速度也快。例如,上面這段搜集作者名的代碼處理一個2MB的數(shù)據(jù)文件時間不會超過1秒。另外,這不是偶然的,數(shù)據(jù)描述是Lua的主要應(yīng)用之一,從Lua發(fā)明以來,我們花了很多心血使他能夠更快的編譯和運行大的chunks。
序列化
我們經(jīng)常需要序列化一些數(shù)據(jù),為了將數(shù)據(jù)轉(zhuǎn)換為字節(jié)流或者字符流,這樣我們就可以保存到文件或者通過網(wǎng)絡(luò)發(fā)送出去。我們可以在Lua代碼中描述序列化的數(shù)據(jù),在這種方式下,我們運行讀取程序即可從代碼中構(gòu)造出保存的值。
通常,我們使用這樣的方式varname = <exp>來保存一個全局變量的值。varname部分比較容易理解,下面我們來看看如何寫一個產(chǎn)生值的代碼。對于一個數(shù)值來說:
- function serialize (o)
- if type(o) == "number" then
- io.write(o)
- else ...
- end
對于字符串值而言,原始的寫法應(yīng)該是:
- if type(o) == "string" then
- io.write("'", o, "'")
然而,如果字符串包含特殊字符(比如引號或者換行符),產(chǎn)生的代碼將不是有效的Lua程序。這時候你可能用下面方法解決特殊字符的問題:
- if type(o) == "string" then
- io.write("[[", o, "]]")
千萬不要這樣做!雙引號是針對手寫的字符串的而不是針對自動產(chǎn)生的字符串。如果有人惡意的引導(dǎo)你的程序去使用" ]]..os.execute('rm *')..[[ "這樣的方式去保存某些東西(比如它可能提供字符串作為地址)你最終的chunk將是這個樣子:
- varname = [[ ]]..os.execute('rm *')..[[ ]]
如果你load這個數(shù)據(jù),運行結(jié)果可想而知的。為了以安全的方式引用任意的字符串,string標(biāo)準(zhǔn)庫提供了格式化函數(shù)專門提供"%q"選項。它可以使用雙引號表示字符串并且可以正確的處理包含引號和換行等特殊字符的字符串。這樣一來,我們的序列化函數(shù)可以寫為:
- function serialize (o)
- if type(o) == "number" then
- io.write(o)
- elseif type(o) == "string" then
- io.write(string.format("%q", o))
- else ...
- end
保存不帶循環(huán)的table
我們下一個艱巨的任務(wù)是保存表。根據(jù)表的結(jié)構(gòu)不同,采取的方法也有很多。沒有一種單一的算法對所有情況都能很好地解決問題。簡單的表不僅需要簡單的算法而且輸出文件也需要看起來美觀。
我們第一次嘗試如下:
- 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
盡管代碼很簡單,但很好地解決了問題。只要表結(jié)構(gòu)是一個樹型結(jié)構(gòu)(也就是說,沒有共享的子表并且沒有循環(huán)),上面代碼甚至可以處理嵌套表(表中表)。對于所進(jìn)不整齊的表我們可以少作改進(jìn)使結(jié)果更美觀,這可以作為一個練習(xí)嘗試一下。
(提示:增加一個參數(shù)表示縮進(jìn)的字符串,來進(jìn)行序列化)。前面的函數(shù)假定表中出現(xiàn)的所有關(guān)鍵字都是合法的標(biāo)示符。如果表中有不符合Lua語法的數(shù)字關(guān)鍵字或者字符串關(guān)鍵字,上面的代碼將碰到麻煩。一個簡單的解決這個難題的方法是將:
- io.write(" ", k, " = ")
- 改為
- io.write(" [")
- serialize(k)
- io.write("] = ")
這樣一來,我們改善了我們的函數(shù)的健壯性,比較一下兩次的結(jié)果:
- result of serialize{a=12, b='Lua', key='another "one"'}
第一個版本
- {
- a = 12,
- b = "Lua",
- key = "another \"one\"",
- }
第二個版本
- {
- ["a"] = 12,
- ["b"] = "Lua",
- ["key"] = "another \"one\"",
- }
我們可以通過測試每一種情況,看是否需要方括號,另外,我們將這個問題留作一個練習(xí)給大家。
保存帶有循環(huán)的table
針對普通拓?fù)涓拍钌系膸в醒h(huán)表和共享子表的table,我們需要另外一種不同的方法來處理。構(gòu)造器不能很好地解決這種情況,我們不使用。為了表示循環(huán)我們需要將表名記錄下來,下面我們的函數(shù)有兩個參數(shù):table和對應(yīng)的名字。另外,我們還必須記錄已經(jīng)保存過的table以防止由于循環(huán)而被重復(fù)保存。我們使用一個額外的table來記錄保存過的表的軌跡,這個表的下表索引為table,而值為對應(yīng)的表名。
我們做一個限制:要保存的table只有一個字符串或者數(shù)字關(guān)鍵字。下面的這個函數(shù)序列化基本類型并返回結(jié)果。
- function basicSerialize (o)
- if type(o) == "number" then
- return tostring(o)
- else -- assume it is a string
- return string.format("%q", o)
- end
- end
關(guān)鍵內(nèi)容在接下來的這個函數(shù),saved這個參數(shù)是上面提到的記錄已經(jīng)保存的表的蹤跡的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
調(diào)用save('a', a)之后結(jié)果為:
- 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遍歷的順序,不過,這個算法保證了一個新的定義中需要的前面的節(jié)點都已經(jīng)被定義過)
如果我們想保存帶有共享部分的表,我們可以使用同樣table的saved參數(shù)調(diào)用save函數(shù),例如我們創(chuàng)建下面兩個表:
- a = {{"one", "two"}, 3}
- b = {k = a[1]}
保存它們:
- save('a', a)
- save('b', b)
結(jié)果將分別包含相同部分:
- 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表來調(diào)用save函數(shù):
- local t = {}
- save('a', a, t)
- save('b', b, t)
結(jié)果將共享相同部分:
- a = {}
- a[1] = {}
- a[1][1] = "one"
- a[1][2] = "two"
- a[2] = 3
- b = {}
- b["k"] = a[1]
上面這種方法是Lua中常用的方法,當(dāng)然也有其他一些方法可以解決問題。比如,我們可以不使用全局變量名來保存,即使用封包,用chunk構(gòu)造一個local值然后返回之;通過構(gòu)造一張表,每張表名與其對應(yīng)的函數(shù)對應(yīng)起來等。Lua給予你權(quán)力,由你決定如何實現(xiàn)。
小結(jié):詳解LUA腳本語言之數(shù)據(jù)文件與持久化的內(nèi)容介紹完了,希望通過本文的學(xué)習(xí)能對你有所幫助!