分割與組合

lua-users home
wiki

「分割」[1]與「組合」[2]是兩個常見的字串運算子,它們基本上是彼此的反向運算。分割會將包含分隔符號的字串分割為由該分隔符號分隔的子字串清單。組合會將清單中的字串合併成一個新的字串,並在每個字串之間插入分隔符號。

以下是各種在 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("", "") 的反向運算應該是回傳什麼,並不明顯。

注意:完全忽略空的值並不好,例如 CSV 檔案中的列資料,其中的欄位位置很重要。包含空列 "" 的 CSV 檔案會讓人無法清楚判斷:這是一個包含空值的欄位,還是有零個欄位?雖然不太可能,但也不排除 CSV 檔案有零個欄位。

是否應該有一個自變數來限制分割的次數?

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" }

(PhilippeLhoste)


函數:類似 Perl 的 split/join

許多人在 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

(PeterPrade)


功能:Perl 類似之 split/join 的替代方案

這是以供比較的自訂 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

(GavinKistner)


功能:類似 PHP 的 explode

使用分隔符號來將字串分割成表格(從 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
(Lance Li)


功能:使用元表和 __index

這個功能使用元表的 __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 的行為

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!@

(JoanOrdinas)


使用協程

若我們將 split 簡單地定義為「取出所有 0-n 個字元、接著是一個分隔符號的出現,再加上剩下的字串」,我相信這樣可產生最直覺式的區段邏輯,接著我們就能得到一個簡單的實作,僅透過 gmatch,它涵蓋了所有案例而且仍然允許分隔符號是一個樣式

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'})

(CosminApreutesei)


-- 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.3 中的語意更改

對於 Lua 5.3.2,在「大多數情境」下區段都是棘手的,因為 string.gmatchstring.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)

DirkLaurie


解決所有由未固定錨點的 '.-' 搜尋樣式所造成的效能問題

在先前的章節中,'.-' (非貪婪) 模式的糟糕效能,可以透過將其繫結到搜尋字串的起始位置而解決 (起始位置不一定就是字串中的第一個位置,如果我們使用 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 會等於起始位置 pr 會是分隔符號最後一個字元的索引 (用於下一個迴圈),而且 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 參數用於限制傳回陣列中的項目數量 (如果達到這個限制,傳回的最後一個分隔字串不會包含分隔符號的任何出現,因此在這個實作中會忽略文字的其餘部分)。另請注意,可能會傳回由分隔符號分隔的空字串 (多達與找到的分隔符號出現次數一樣多個空字串)

因此

可以使用純文字分隔符號 (例如具有單一編碼的 '\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 參數(例如在上面表現得好像它為 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

當模式為空白字串時,該版本將再次失敗。其他語言中 split 函式的規格定義了這些角落案例的行為方式(見上方「和其他語言的比較」)。--David Manura


另見


RecentChanges · 偏好設定
編輯 · 歷程記錄
最後編輯時間為 2020 年 6 月 2 日晚上 11:42 (格林威治標準時間) (差異)