分割合并

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 使用模式来匹配非分隔符文本,而不是分隔符本身。

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 :) 如果 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 的问题是,它无法处理分隔符模式未出现在字符串末尾的情况。在这种情况下,最终的 "(.-)" 永远无法捕获字符串的末尾,因为整体模式无法匹配。为了处理这种情况,您必须做一些更复杂的事情。下面的 split 函数的行为或多或少类似于 perl 或 python 中的 split。特别是,字符串开头和结尾的单个匹配不会创建新元素。连续的多个匹配会创建空字符串元素。

-- 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 的 split/join 函数。以下是我的函数

-- 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 函数,用于比较。它与上面的函数基本相同;不是那么 DRY,但(IMO)稍微干净一些。它不使用 gfind(如以下建议),因为我想能够为 split 字符串指定一个模式,而不是为数据部分指定一个模式。如果速度至关重要,可以通过将 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
]]


函数:split 的真实 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!@']

IMO,此 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

还要注意,以上这些函数在返回表中会去除所有分隔符。您可能希望有一个名为“separators”的变量,以便在需要时获取副本以实现不同的行为。修改很简单:在上面三个函数的 `while` 循环中,只需追加两个字符串,而不是一个:分隔后的单词将位于返回表中的奇数位置(从 1 开始),而分隔符将位于偶数位置(从 2 开始),如果有出现的话。

这允许创建一个简单的词法分析器,其中“分隔符”(定义为如上所述的“扩展模式”或模式表)将是词法标记,而“非分隔符”将是额外的可选空格,不会被标记匹配。在下面的示例代码中,扩展模式使用空字符(在 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


另请参见


最近更改 · 偏好设置
编辑 · 历史
最后编辑于 2020 年 6 月 2 日下午 11:42 格林威治标准时间 (差异)