分割與組合 |
|
以下是各種在 Lua 中設計和實作這些函式的說明。
在 Lua 5.x 中,可以使用 table.concat[3]進行組合:table.concat(tbl, delimiter_str)
。
table.concat({"a", "b", "c"}, ",") --> "a,b,c"
其它介面也是可行的,在很大程度上取決於所選擇的分割介面,因為組合通常會用作分割的反向運算。
首先,雖然 Lua 的標準函式庫中沒有分割函式,但它有提供 string.gmatch
[4],在許多情況中,可以使用 string.gmatch
代替分割函式。相較於分割函式,string.gmatch
會採用樣式來比對非分隔符號文字,而不是分隔符號本身
local example = "an example string" for i in string.gmatch(example, "%S+") do print(i) end -- output: -- an -- example -- string
split
[1]函式會將字串分割成子字串清單,並根據特定分隔符號(字元、字元組或樣式)來切斷原始字串。有許多種方式可以設計字串分割函式。以下說明設計決定的摘要。
分割應該回傳一個表格陣列、一個清單或一個反覆運算器?
split("a,b,c", ",") --> {"a", "b", "c"} split("a,b,c", ",") --> "a","b","c" (not scalable: Lua has a limit of a few thousand return values) for x in split("a,b,c", ",") do ..... end
分隔符號應該是一個字串、Lua 樣式、LPeg 樣式或正規表示式?
split("a +b c", " +") --> {"a ", "b c"} split("a +b c", " +") --> {"a", "+b", "c"} split("a +b c", some_other_object) --> .....
應該如何處理空的分隔符號?
split("abc", "") --> {"a", "b", "c"} split("abc", "") --> {"", "a", "b", "c", ""} split("abc", "") --> error split("abc", "%d*") --> what about patterns that can evaluate to empty strings?
split(s,"")
是一種將字串分割成字元的方便慣例。在 Lua 中,我們也可以改用 for c in s:gmatch"." do ..... end
。應該如何處理空的數值?
split(",,a,b,c,", ",") --> {"a", "b", "c"} split(",,a,b,c,", ",") --> {"", "", "a", "b", "c", ""} split(",", ",") --> {} or {""} or {"", ""} ? split("", ",") --> {} or {""} ?
join({"",""}, "")
、join({""}, "")
和 join({}, "")
都會產生相同的字串 ""
。因此,split("", "")
的反向運算應該是回傳什麼,並不明顯。
是否應該有一個自變數來限制分割的次數?
split("a,b,c", ",", 2) --> {"a", "b,c"}
是否應該回傳分隔符號?當分隔符號是樣式時,分隔符號可能會改變,這時回傳分隔符號比較實用。
split("a b c", " +") --> {"a", " ", "b", " ", "c"}
string.gmatch
[5] 是 split
的一種對偶,傳回符號樣式相符的子字串,並捨棄它們之間的字串,而不是相反。傳回兩者結果的函數有時稱為 partition
[6]。
string.gsub
/string.match
依據樣式分割在出現單一字元時,將字串中斷開。如果已知欄位的數目
str:match( ("([^"..sep.."]*)"..sep):rep(nsep) )
如果欄位的數目未知
fields = {str:match((str:gsub("[^"..sep.."]*"..sep, "([^"..sep.."]*)"..sep)))}
有些人可能將上述內容稱為 hack :) 如果它是一個樣式 meta 字元,則 sep
需要進行跳脫,而且您會做更好的預先運算和/或記憶樣式。而且它會捨棄掉最後一個分隔符號之後的值。例如:「a,b,c」傳回「a」和「b」,但不傳回「c」
string.gsub
fields = {} str:gsub("([^"..sep.."]*)"..sep, function(c) table.insert(fields, c) end)
無法如預期運作
str, sep = "1:2:3", ":" fields = {} str:gsub("([^"..sep.."]*)"..sep, function(c) table.insert(fields, c) end) for i,v in ipairs(fields) do print(i,v) end -- output: -- 1 1 -- 2 2
修復
function string:split(sep) local sep, fields = sep or ":", {} local pattern = string.format("([^%s]+)", sep) self:gsub(pattern, function(c) fields[#fields+1] = c end) return fields end
範例:將字串分割成字詞,或傳回 nil
function justWords(str) local t = {} local function helper(word) table.insert(t, word) return "" end if not str:gsub("%w+", helper):find"%S" then return t end end
它使用 sep
樣式來分割字串。它會針對每個區段呼叫 func
。在呼叫 func
時,第一個引數是區段,其餘引數是來自 sep
的擷取結果,如果有任何的話。在最後一個區段中,func
將僅以單一引數呼叫。(它可用作旗標,或您可以使用兩種不同的函數)。sep
不得符合空字串。增強功能會留給各位作為練習 :)
func((str:gsub("(.-)("..sep..")", func)))
範例:使用 DOS 或 Unix 換行符號,將字串分割成行,並在結果中建立一個表格。
function lines(str) local t = {} local function helper(line) table.insert(t, line) return "" end helper((str:gsub("(.-)\r?\n", helper))) return t end
在上列函數中使用 gsub 的問題在於它無法處理分隔符號樣式未出現在字串結尾的情況。在這種情況下,最終的「(.-)」永遠無法擷取到字串的結尾,因為整體樣式未匹配成功。若要處理此類情況,您必須進行一些稍嫌複雜一點的動作。下列的分割函數有類似 perl 或 python 中分割函數的功能。特別是,在字串的開頭和結尾處的單一匹配不會建立新的元素。連續的多重匹配會建立空字串元素。
-- Compatibility: Lua-5.1 function split(str, pat) local t = {} -- NOTE: use {n = 0} in Lua-5.0 local fpat = "(.-)" .. pat local last_end = 1 local s, e, cap = str:find(fpat, 1) while s do if s ~= 1 or cap ~= "" then table.insert(t, cap) end last_end = e+1 s, e, cap = str:find(fpat, last_end) end if last_end <= #str then cap = str:sub(last_end) table.insert(t, cap) end return t end
範例:將檔案路徑字串分割成組件。
function split_path(str) return split(str,'[\\/]+') end parts = split_path("/usr/local/bin") --> {'usr','local','bin'}
測試案例
split('foo/bar/baz/test','/') --> {'foo','bar','baz','test'} split('/foo/bar/baz/test','/') --> {'foo','bar','baz','test'} split('/foo/bar/baz/test/','/') --> {'foo','bar','baz','test'} split('/foo/bar//baz/test///','/') --> {'foo','bar','','baz','test','',''} split('//foo////bar/baz///test///','/+') --> {'foo','bar','baz','test'} split('foo','/+') --> {'foo'} split('','/+') --> {} split('foo','') -- opps! infinite loop!
在郵寄清單中討論了這個主題之後,我製作了自己的函數... 我不知不覺中用了一種與上述函數類似的方式,只不過我使用 gfind 來反覆運算,而且我將字串開頭和結尾處的單一匹配視為空欄位。同上,連續的數個定界符號會建立空字串元素。
-- Compatibility: Lua-5.0 function Split(str, delim, maxNb) -- Eliminate bad cases... if string.find(str, delim) == nil then return { str } end if maxNb == nil or maxNb < 1 then maxNb = 0 -- No limit end local result = {} local pat = "(.-)" .. delim .. "()" local nb = 0 local lastPos for part, pos in string.gfind(str, pat) do nb = nb + 1 result[nb] = part lastPos = pos if nb == maxNb then break end end -- Handle the last field if nb ~= maxNb then result[nb + 1] = string.sub(str, lastPos) end return result end
測試案例
ShowSplit("abc", '') --> { [1] = "", [2] = "", [3] = "", [4] = "", [5] = "" } -- No infite loop... but garbage in, garbage out... ShowSplit("", ',') --> { [1] = "" } ShowSplit("abc", ',') --> { [1] = "abc" } ShowSplit("a,b,c", ',') --> { [1] = "a", [2] = "b", [3] = "c" } ShowSplit("a,b,c,", ',') --> { [1] = "a", [2] = "b", [3] = "c", [4] = "" } ShowSplit(",a,b,c,", ',') --> { [1] = "", [2] = "a", [3] = "b", [4] = "c", [5] = "" } ShowSplit("x,,,y", ',') --> { [1] = "x", [2] = "", [3] = "", [4] = "y" } ShowSplit(",,,", ',') --> { [1] = "", [2] = "", [3] = "", [4] = "" } ShowSplit("x!yy!zzz!@", '!', 4) --> { [1] = "x", [2] = "yy", [3] = "zzz", [4] = "@" } ShowSplit("x!yy!zzz!@", '!', 3) --> { [1] = "x", [2] = "yy", [3] = "zzz" } ShowSplit("x!yy!zzz!@", '!', 1) --> { [1] = "x" } ShowSplit("a:b:i:p:u:random:garbage", ":", 5) --> { [1] = "a", [2] = "b", [3] = "i", [4] = "p", [5] = "u" } ShowSplit("hr , br ; p ,span, div", '%s*[;,]%s*') --> { [1] = "hr", [2] = "br", [3] = "p", [4] = "span", [5] = "div" }
許多人在 Lua 中錯過了類似 Perl 的分割/聯合函數。以下是我的
-- Concat the contents of the parameter list, -- separated by the string delimiter (just like in perl) -- example: strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"}) function strjoin(delimiter, list) local len = getn(list) if len == 0 then return "" end local string = list[1] for i = 2, len do string = string .. delimiter .. list[i] end return string end -- Split text into a list consisting of the strings in text, -- separated by strings matching delimiter (which may be a pattern). -- example: strsplit(",%s*", "Anna, Bob, Charlie,Dolores") function strsplit(delimiter, text) local list = {} local pos = 1 if strfind("", delimiter, 1) then -- this would result in endless loops error("delimiter matches empty string!") end while 1 do local first, last = strfind(text, delimiter, pos) if first then -- found? tinsert(list, strsub(text, pos, first-1)) pos = last+1 else tinsert(list, strsub(text, pos)) break end end return list end
這是以供比較的自訂 split 功能。它與上述的 largely 雷同;並非那麼 DRY,但(依我個人意見)稍微乾淨一些。它不使用 gfind(如下建議),因為我想要能夠指定區段字串的樣式,而非資料區段的樣式。如果速度至關重要,可透過將 string.find 快取為一個當地變數 'strfind'(如同上述)來讓速度變快。
--Written for 5.0; could be made slightly cleaner with 5.1 --Splits a string based on a separator string or pattern; --returns an array of pieces of the string. --(May optionally supply a table as the third parameter which will be filled with the results.) function string:split( inSplitPattern, outResults ) if not outResults then outResults = { } end local theStart = 1 local theSplitStart, theSplitEnd = string.find( self, inSplitPattern, theStart ) while theSplitStart do table.insert( outResults, string.sub( self, theStart, theSplitStart-1 ) ) theStart = theSplitEnd + 1 theSplitStart, theSplitEnd = string.find( self, inSplitPattern, theStart ) end table.insert( outResults, string.sub( self, theStart ) ) return outResults end
使用分隔符號來將字串分割成表格(從 TableUtils 移轉)
-- explode(seperator, string) function explode(d,p) local t, ll t={} ll=0 if(#p == 1) then return {p} end while true do l = string.find(p, d, ll, true) -- find the next d in the string if l ~= nil then -- if "not not" found then.. table.insert(t, string.sub(p,ll,l-1)) -- Save it in our array. ll = l + 1 -- save just after where we found it for searching next time. else table.insert(t, string.sub(p,ll)) -- Save what's left in our array. break -- Break at end, as it should be, according to the lua manual. end end return t end
這是支援限制的 PHP 風格 explode 的版本
function explode(sep, str, limit) if not sep or sep == "" then return false end if not str then return false end limit = limit or mhuge if limit == 0 or limit == 1 then return {str}, 1 end local r = {} local n, init = 0, 1 while true do local s,e = strfind(str, sep, init, true) if not s then break end r[#r+1] = strsub(str, init, s - 1) init = e + 1 n = n + 1 if n == limit - 1 then break end end if init <= strlen(str) then r[#r+1] = strsub(str, init) else r[#r+1] = "" end n = n + 1 if limit < 0 then for i=n, n + limit + 1, -1 do r[i] = nil end n = n + limit end return r, n end
這個功能使用元表的 __index 功能來填入區段部分的表格。這個功能不會嘗試(正確地)反轉樣式,因此實際上無法像大多數的字串區段功能一樣工作。
--[[ written for Lua 5.1 split a string by a pattern, take care to create the "inverse" pattern yourself. default pattern splits by white space. ]] string.split = function(str, pattern) pattern = pattern or "[^%s]+" if pattern:len() == 0 then pattern = "[^%s]+" end local parts = {__index = table.insert} setmetatable(parts, parts) str:gsub(pattern, parts) setmetatable(parts, nil) parts.__index = nil return parts end -- example 1 str = "no separators in this string" parts = str:split( "[^,]+" ) print( # parts ) table.foreach(parts, print) --[[ output: 1 1 no separators in this string ]] -- example 2 str = " split, comma, separated , , string " parts = str:split( "[^,%s]+" ) print( # parts ) table.foreach(parts, print) --[[ output: 4 1 split 2 comma 3 separated 4 string ]]
這是 Python 的行為
Python 2.5.1 (r251:54863, Jun 15 2008, 18:24:51) [GCC 4.3.0 20080428 (Red Hat 4.3.0-8)] on linux2 >>> 'x!yy!zzz!@'.split('!') ['x', 'yy', 'zzz', '@'] >>> 'x!yy!zzz!@'.split('!', 3) ['x', 'yy', 'zzz', '@'] >>> 'x!yy!zzz!@'.split('!', 2) ['x', 'yy', 'zzz!@'] >>> 'x!yy!zzz!@'.split('!', 1) ['x', 'yy!zzz!@']
依我個人的拙見,這個 Lua 功能實作了這個語意
function string:split(sSeparator, nMax, bRegexp) assert(sSeparator ~= '') assert(nMax == nil or nMax >= 1) local aRecord = {} if self:len() > 0 then local bPlain = not bRegexp nMax = nMax or -1 local nField, nStart = 1, 1 local nFirst,nLast = self:find(sSeparator, nStart, bPlain) while nFirst and nMax ~= 0 do aRecord[nField] = self:sub(nStart, nFirst-1) nField = nField+1 nStart = nLast+1 nFirst,nLast = self:find(sSeparator, nStart, bPlain) nMax = nMax-1 end aRecord[nField] = self:sub(nStart) end return aRecord end
觀察使用單純字串或正規表示式作為分隔符號的可能性。
測試案例
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio ... > for k,v in next, string.split('x!yy!zzz!@', '!') do print(v) end x yy zzz @ > for k,v in next, string.split('x!yy!zzz!@', '!', 3) do print(v) end x yy zzz @ > for k,v in next, string.split('x!yy!zzz!@', '!', 2) do print(v) end x yy zzz!@ > for k,v in next, string.split('x!yy!zzz!@', '!', 1) do print(v) end x yy!zzz!@
function gsplit(s,sep) return coroutine.wrap(function() if s == '' or sep == '' then coroutine.yield(s) return end local lasti = 1 for v,i in s:gmatch('(.-)'..sep..'()') do coroutine.yield(v) lasti = i end coroutine.yield(s:sub(lasti)) end) end -- same idea without coroutines function gsplit2(s,sep) local lasti, done, g = 1, false, s:gmatch('(.-)'..sep..'()') return function() if done then return end local v,i = g() if s == '' or sep == '' then done = true return s end if v == nil then done = true return s:sub(lasti) end lasti = i return v end end The {{gsplit()}} above returns an iterator, so other API variants can be easily derived from it: {{{!Lua function iunpack(i,s,v1) local function pass(...) local v1 = i(s,v1) if v1 == nil then return ... end return v1, pass(...) end return pass() end function split(s,sep) return iunpack(gsplit(s,sep)) end function accumulate(t,i,s,v) for v in i,s,v do t[#t+1] = v end return t end function tsplit(s,sep) return accumulate({}, gsplit(s,sep)) end
請注意,上述實作不允許在分隔符號內擷取。若要允許這樣做,必須建立另一個閉包來傳遞額外的擷取字串(請參閱 VarargTheSecondClassCitizen)。語意也變得混亂(我認為一個用例可能是想要知道每個字串的實際分隔符號是什麼,例如對於 [%.,;] 的分隔符號樣式來說)。
function gsplit(s,sep) local i, done, g = 1, false, s:gmatch('(.-)'..sep..'()') local function pass(...) if ... == nil then done = true return s:sub(i) end i = select(select('#',...),...) return ... end return function() if done then return end if s == '' or sep == '' then done = true return s end return pass(g()) end end
上述實作的問題在於儘管容易閱讀,但 Lua 中的 (.-) 樣式效能極差,因此以下僅基於 string.find 的實作(允許在分隔符號中擷取並新增一個第三個引數「plain」,類似於 string.find)
function string.gsplit(s, sep, plain) local start = 1 local done = false local function pass(i, j, ...) if i then local seg = s:sub(start, i - 1) start = j + 1 return seg, ... else done = true return s:sub(start) end end return function() if done then return end if sep == '' then done = true return s end return pass(s:find(sep, start, plain)) end end
單元測試
local function test(s,sep,expect) local t={} for c in s:gsplit(sep) do table.insert(t,c) end assert(#t == #expect) for i=1,#t do assert(t[i] == expect[i]) end test(t, expect) end test('','',{''}) test('','asdf',{''}) test('asdf','',{'asdf'}) test('', ',', {''}) test(',', ',', {'',''}) test('a', ',', {'a'}) test('a,b', ',', {'a','b'}) test('a,b,', ',', {'a','b',''}) test(',a,b', ',', {'','a','b'}) test(',a,b,', ',', {'','a','b',''}) test(',a,,b,', ',', {'','a','','b',''}) test('a,,b', ',', {'a','','b'}) test('asd , fgh ,; qwe, rty. ,jkl', '%s*[,.;]%s*', {'asd','fgh','','qwe','rty','','jkl'}) test('Spam eggs spam spam and ham', 'spam', {'Spam eggs ',' ',' and ham'})
-- single char string splitter, sep *must* be a single char pattern -- *probably* escaped with % if it has any special pattern meaning, eg "%." not "." -- so good for splitting paths on "/" or "%." which is a common need local function csplit(str,sep) local ret={} local n=1 for w in str:gmatch("([^"..sep.."]*)") do ret[n] = ret[n] or w -- only set once (so the blank after a string is ignored) if w=="" then n = n + 1 end -- step forwards on a blank but not a string end return ret end -- the following is true of any string, csplit will do the reverse of a concat local str="" print(str , assert( table.concat( csplit(str,"/") , "/" ) == str ) ) local str="only" print(str , assert( table.concat( csplit(str,"/") , "/" ) == str ) ) local str="/test//ok/" print(str , assert( table.concat( csplit(str,"/") , "/" ) == str ) ) local str=".test..ok." print(str , assert( table.concat( csplit(str,"%.") , "." ) == str ) )
對於 Lua 5.3.2,在「大多數情境」下區段都是棘手的,因為 string.gmatch
和 string.gsub
會引入不必要的額外空白欄位(如同 Perl)。從 Lua 5.3.3 開始,它們不再會這樣做,它們現在的行為如同 Python。因此,以下極簡區段功能現在是 table.concat
的真反函數;先前它不是。
-- splits 'str' into pieces matching 'pat', returns them as an array local function split(str,pat) local tbl = {} str:gsub(pat, function(x) tbl[#tbl+1]=x end) return tbl end local str = "a,,b" -- comma-separated list local pat = "[^,]*" -- everything except commas assert (table.concat(split(str, pat), ",") == str)
'.-'
搜尋樣式所造成的效能問題'.-'
(非貪婪) 模式的糟糕效能,可以透過將其繫結到搜尋字串的起始位置而解決 (起始位置不一定就是字串中的第一個位置,如果我們使用 string.find()
函數及其第三個參數),因此用於匹配分隔符號的子模式可以是貪婪的 (但請注意,如果分隔符號是可以匹配空字串的模式,會在文字開頭之前找到一個空匹配,空分隔的字串和空的分隔符號,所以這可能會產生一個無限迴圈:不要為可匹配空字串的分隔符號指定任何模式)。因此要從起始位置 p
在字串 str
中搜尋第一個分隔符號 ;
,我們可以使用
q, r = str:find('^.-;', p)
同樣地,當分隔符號是靜態分隔符號時,我們不需要任何擷取來呼叫 string.find()
:如果有一個匹配,q 會等於 p (因為模式繫結在開始處),而且 r 會恰好在分隔符號的最後一個字元上。由於我們可以在開始迴圈前先用一個簡單的初始化 k = #sep
來決定分隔符號的長度來分割字串,因此新的分隔字串會是 str:sub(q, r - k)
。但是,靜態純文字分隔符號必須先在搜尋模式中轉換,透過使用前置詞 '%'
來跳脫其「特殊字元」。
然而,如果分隔符號必須是模式,則找到的有效分隔符號可能有可變長度,因此您需要擷取在分隔符號前的文字,以及要搜尋的完整模式是 ('^(.-)' .. sep)
q, r, s = str:find('^(.-);', p)
如果出現匹配,q
會等於起始位置 p
,r
會是分隔符號最後一個字元的索引 (用於下一個迴圈),而且 s
會是第一個擷取,亦即開始於位置 p
(或 q
) 但在未擷取分隔符號之前的字串。
這會得出以下的高效能函數
function split(str, sep, plain, max) local result, count, first, found, last, word = {}, 1, 1 if plain then sep = sep:gsub('[$%%()*+%-.?%[%]^]', '%%%0') end sep = '^(.-)' .. sep repeat found, last, word = str:find(sep, first) if q then result[count], count, first = word, count + 1, last + 1 else result[count] = str:sub(first) break end until count == max return result end
與先前的函數相同,您可以傳遞一個選用參數 plain
,將其設定為 true 以搜尋純文字分隔符號 (將轉換為模式),並將 max
參數用於限制傳回陣列中的項目數量 (如果達到這個限制,傳回的最後一個分隔字串不會包含分隔符號的任何出現,因此在這個實作中會忽略文字的其餘部分)。另請注意,可能會傳回由分隔符號分隔的空字串 (多達與找到的分隔符號出現次數一樣多個空字串)
因此
split(';;A', ';')
會傳回 {"", "", "A"}
split(';;A', ';', true, 2)
會傳回 {"", ""}
可以使用純文字分隔符號 (例如具有單一編碼的 '\n'
換行符、單一 ';'
分號或單一 '\t'
制表符控制項,或更長的序列,例如 '--'
) 的最精簡分割函數如下
local function splitByPlainSeparator(str, sep, max) local z = #sep; sep = '^.-'..sep:gsub('[$%%()*+%-.?%[%]^]', '%%%0') local t,n,p, q,r = {},1,1, str:find(sep) while q and n~=max do t[n],n,p = s:sub(q,r-z),n+1,r+1 q,r = str:find(sep,p) end t[n] = str:sub(p) return t end
可用於含有分割樣式的函式中最簡潔的分裂函式(例如一個變數的新行,例如 '\r?[\n\v\f]'
或任何空白字元序列,例如 '%s+'
或逗號,其周圍可能被貪心空白字元包圍,例如 '%s*,%s*'
),就是這個函式。
local function splitByPatternSeparator(str, sep, max) sep = '^(.-)'..sep local t,n,p, q,r,s = {},1,1, str:find(sep) while q and n~=max do t[n],n,p = s,n+1,r+1 q,r,s = str:find(sep,p) end t[n] = str:sub(p) return t end
然而,這個後一個函式仍不支援分割符號可以是多個選項之一(因為 Lua 在其樣式中沒有 |
),但你可以透過使用多個樣式來迴避這個限制,並在一個內部子迴圈中使用 str:find()
來找出每個可能的選項並取找到的最小位置,對每個選項樣式使用一個很小的迴圈(例如使用延伸樣式進行分裂 '\r?\n|\r|<br%s*/?>'
)。
local function splitByExtendedPatternSeparator(str, seps, max) -- Split the extended pattern into a sequence of Lua patterns, using the function defined above. -- Note: '|' cannot be part of any subpattern alternative for the separator (no support here for any escape). -- Alternative: just pass "seps" as a sequence of standard Lua patterns built like below, with a non-greedy -- pattern anchored at start for the contextual text accepted in the returned texts betweeen separators, -- and the empty capture '()' just before the pattern for a single separator. if type(seps) == 'string' then seps = splitByPlainSeparator(sep, '|') -- Adjust patterns for i, sep in ipairs(seps) do seps[i] = '^.-()' .. sep end end -- Now the actual loop to split the first string parameter local t, n, p = {}, 1, 1 while n ~= max do -- locate the nearest subpatterns that match a separator in str:sub(p); -- if two subpatterns match at same nearest position, keep the longest one local first, last = nil for i, sep in ipairs(seps) do local q, r, s = str:find(sep, p) if q then -- A possible separator (not necessarily the neareast) was found in str:sub(s, r) -- Here: q~=nil, r~=nil, s~=nil, q==p <= s <= r) if not first or s < first then first, last = s, r -- this also overrides any longer pattern, but located later elseif r > last then last = r -- prefer the longest pattern at the same position end end end if not first then break end -- the nearest separator (with the longest length) was found in str:sub(first, last) t[n], n, p = str:sub(p, first - 1), n + 1, last + 1 end t[n] = str:sub(p) return t end
最後三個函式(幾乎相等,但功能不完全相同),均可以搜尋任何分隔符號(不限於一個字元)的所有出現,它們還有一個可選的 max
參數(僅用於單一 while
陳述式的條件)。
max
設為大於 1 的正整數,則傳回的子字串數量不會超過這個數字,最後一個子字串包含所有其他文字(包括分隔符號),因此在找到第一個 (max-1) 個出現之後便停止分裂;max==1
,它們都會傳回原始文字;max
為 nil,或不是整數,或為負數或零,則會掃描所有分隔符號,且傳回的資料表將有 K+1 個字串項目,如果來源文字包含 k 個分隔符號的出現,或如果它完全沒有分隔符號的出現,則傳回一個單一字串,與輸入文字相同。str
和 sep
,而且它們都應該是非 nil 的字串;但是 max
是可選的,而且為了產生效用,它應該是正整數。如果你從不想要 max
參數(例如在上面表現得好像它為 nil
,因此將完整文字分裂成移除普通或樣式分隔符號的所有出現),只要移除這些 while
陳述式第一行中的條件 and n~=max
。
請注意,上述函式也會在回傳表中刪除所有分隔符號。你可能想要有變數「分隔符號」,你會想要取得一份副本以獲得不同的行為。這個修改非常簡單:在上述 3 個函式的 while
迴圈中,只要附加兩個字串,而不要只附加一個:已分隔的字詞將在傳回的表中出現在奇數位置(從 1 開始)(其中將增加數量的項目),並且如果出現,分隔符號將出現在偶數位置(從 2 開始)。
這允許建立一個簡單的詞法剖析器,其中「分隔器」(定義為如上方的「延伸模式」或模式表)將是詞法符號,且「非分隔器」將會是符號未比對到的額外可選白空白,範例程式碼中,延伸模式使用 null 字元 (Lua 字串的字串常數中為 '\000'
) 而不是管線符號,來區分比對個別符號的備用子模式。
local function splitTokens(str, tokens, max) -- Split the extended pattern into a sequence of Lua patterns, using the function defined above. -- Note: '\000' cannot be part of any subpattern alternative for the separator (no support here for any escape). -- Alternative: just pass "seps" as a sequence of standard Lua patterns built like below, with a non-greedy -- pattern anchored at start for the contextual text accepted in the returned texts betweeen separators, -- and the empty capture '()' just before the pattern for a single separator. if type(tokens) == 'string' then tokens = splitByPlainSeparator(tokens, '\000') -- Adjust patterns for i, token in ipairs(tokens) do tokens[i] = '^.-()' .. token end end -- Now the actual loop to split the first string parameter local t, n, p = {}, 1, 1 while n ~= max do -- locate the nearest subpatterns that match a separator in str:sub(p); -- if two subpatterns match at same nearest position, keep the longest one local first, last = nil for i, token in ipairs(tokens) do local q, r, s = str:find(token, p) if q then -- A possible token (not necessarily the neareast) was found in str:sub(s, r) -- Here: q~=nil, r~=nil, s~=nil, q==p <= s <= r) if not first or s < first then first, last = s, r -- this also overrides any longer pattern, but located later elseif r > last then last = r -- prefer the longest pattern at the same position end end end if not first then break end -- The nearest token (with the longest length) was found in str:sub(first, last). -- Store the non-token part (possibly empty) at odd position, and the token at the next even position t[n], t[n + 1], n, p = str:sub(p, first - 1), str:sub(first, last), n + 2, last + 1 end t[n] = str:sub(p) -- Store the last non-token (possibly empty) at odd position return t end
因此,您可以呼叫這個,舉例來說,來對包含識別碼、整數或浮點數 (例如 Lua 語法中的) 的文字進行符號化,或孤立的非空白符號 (您可以透過將備用選項新增到延伸模式來針對較長的符號新增符號,或支援其他字面值)
splitTokens(str, '[%a_][%w_]+' .. '\000' .. '%d+[Ee][%-%+]?%d+' .. '\000' .. '%d+%.?%d*' .. '\000' .. '%.%d+[Ee][%-%+]?%d+' .. '\000' .. '%.%d+' .. '\000' .. '[^%w_%s]')
(verdy_p)
當然,我沒有不敬的意思,但.. 實際上有人有可運作的 split 函式,且沒有像是無限迴圈、錯誤比對或錯誤的狀況嗎?所有這些「使用方式」有任何幫助嗎? -- CosminApreutesei
嘗試 Rici Lake 的 split 函式:LuaList:2006-12/msg00414.html -- J�rg Richter