捕获
捕获是这样一种机制:可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个捕获。
在string.find使用捕获的时候,函数会返回捕获的值作为额外的结果。这常被用来将一个目标串拆分成多个:
pair = "name = Anna" _, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)") print(key, value) --> name Anna
'%a+' 表示菲空的字母序列;'%s*' 表示0个或多个空白。在上面的例子中,整个模式代表:一个字母序列,后面是任意多个空白,然后是 '=' 再后面是任意多个空白,然后是一个字母序列。两个字母序列都是使用圆括号括起来的子模式,当他们被匹配的时候,他们就会被捕获。当匹配发生的时候,find函数总是先返回匹配串的索引下标(上面例子中我们存储哑元变量 _ 中),然后返回子模式匹配的捕获部分。下面的例子情况类似:
date = "17/7/1990" _, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)") print(d, m, y) --> 17 7 1990
我们可以在模式中使用向前引用,'%d'(d代表1-9的数字)表示第d个捕获的拷贝。
看个例子,假定你想查找一个字符串中单引号或者双引号引起来的子串,你可能使用模式 '["'].-["']',但是这个模式对处理类似字符串 "it's all right" 会出问题。为了解决这个问题,可以使用向前引用,使用捕获的第一个引号来表示第二个引号:
s = [[then he said: "it's all right"!]] a, b, c, quotedPart = string.find(s, "(["'])(.-)%1") print(quotedPart) --> it's all right print(c) --> "
捕获值的第三个应用是用在函数gsub中。与其他模式一样,gsub的替换串可以包含 '%d',当替换发生时他被转换为对应的捕获值。(顺便说一下,由于存在这些情况,替换串中的字符 '%' 必须用 "%%" 表示)。下面例子中,对一个字符串中的每一个字母进行复制,并用连字符将复制的字母和原字母连接起来:
print(string.gsub("hello Lua!", "(%a)", "%1-%1")) --> h-he-el-ll-lo-o L-Lu-ua-a!
print(string.gsub("hello Lua", "(.)(.)", "%2%1")) --> ehll ouLa
让我们看一个更有用的例子,写一个格式转换器:从命令行获取LaTeX风格的字符串,形如:
\command{some text}
<command>some text</command>
s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")
the \quote{task} is to \em{change} that.
the <quote>task</quote> is to change that.
另一个有用的例子是去除字符串首尾的空格:
function trim (s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end
最后一个捕获值应用之处可能是功能最强大的。我们可以使用一个函数作为string.gsub的第三个参数调用gsub。在这种情况下,string.gsub每次发现一个匹配的时候就会调用给定的作为参数的函数,捕获值可以作为被调用的这个函数的参数,而这个函数的返回值作为gsub的替换串。先看一个简单的例子,下面的代码将一个字符串中全局变量$varname出现的地方替换为变量varname的值:
function expand (s) s = string.gsub(s, "$(%w+)", function (n) return _G[n] end) return s endname = "Lua"; status = "great" print(expand("$name is $status, isn't it?"))
--> Lua is great, isn't it?
function expand (s) return (string.gsub(s, "$(%w+)", function (n) return tostring(_G[n]) end)) endprint(expand("print = $print; a = $a"))
--> print = function: 0x8050ce0; a = nil
s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]" print((string.gsub(s, "$(%b[])", function (x) x = "return " .. string.sub(x, 2, -2) local f = loadstring(x) return f() end)))--> sin(3) = 0.1411200080598672; 2^5 = 32
words = {} string.gsub(s, "(%a+)", function (w) table.insert(words, w) end)
{"hello", "hi", "again"}
words = {} for w in string.gfind(s, "(%a)") do table.insert(words, w) end
words = {} for w in string.gfind(s, "%a") do table.insert(words, w) end
name=al&query=a%2Bb+%3D+c&q=yes+or+no
function unescape (s) s = string.gsub(s, "+", " ") s = string.gsub(s, "%%(%x%x)", function (h) return string.char(tonumber(h, 16)) end) return s end
print(unescape("a%2Bb+%3D+c")) --> a+b = c
cgi = {} function decode (s) for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do name = unescape(name) value = unescape(value) cgi[name] = value end end
与解码对应的编码也很容易实现。首先,我们写一个escape函数,这个函数将所有的特殊字符转换成 '%' 后跟字符对应的ASCII码转换成两位的16进制数字(不足两位,前面补0),然后将空白转换为 '+':
function escape (s) s = string.gsub(s, "([&=+%c])", function (c) return string.format("%%%02X", string.byte(c)) end) s = string.gsub(s, " ", "+") return s end
function encode (t) local s = "" for k,v in pairs(t) do s = s .. "&" .. escape(k) .. "=" .. escape(v) end return string.sub(s, 2) -- remove first `&' end t = {name = "al", query = "a+b = c", q="yes or no"} print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al
转换的技巧(Tricks of the Trade)
模式匹配对于字符串操纵来说是强大的工具,你可能只需要简单的调用string.gsub和find就可以完成复杂的操作,然而,因为它功能强大你必须谨慎的使用它,否则会带来意想不到的结果。
对正常的解析器而言,模式匹配不是一个替代品。对于一个quick-and-dirty程序,你可以在源代码上进行一些有用的操作,但很难完成一个高质量的产品。前面提到的匹配C程序中注释的模式是个很好的例子:'/%*.-%*/'。如果你的程序有一个字符串包含了"/*",最终你将得到错误的结果:
test = [[char s[] = "a /* here"; /* a tricky string */]] print(string.gsub(test, "/%*.-%*/", "<COMMENT>")) --> char s[] = "a <COMMENT>
一般情况下,Lua中的模式匹配效率是不错的:一个奔腾333MHz机器在一个有200K字符的文本内匹配所有的单词(30K的单词)只需要1/10秒。但是你不能掉以轻心,应该一直对不同的情况特殊对待,尽可能的更明确的模式描述。一个限制宽松的模式比限制严格的模式可能慢很多。一个极端的例子是模式 '(.-)%$' 用来获取一个字符串内$符号以前所有的字符,如果目标串中存在$符号,没有什么问题;但是如果目标串中不存在$符号。上面的算法会首先从目标串的第一个字符开始进行匹配,遍历整个字符串之后没有找到$符号,然后从目标串的第二个字符开始进行匹配,……这将花费原来平方次幂的时间,导致在一个奔腾333MHz的机器中需要3个多小时来处理一个200K的文本串。可以使用下面这个模式避免上面的问题 '^(.-)%$'。定位符^告诉算法如果在第一个位置没有没找到匹配的子串就停止查找。使用这个定位符之后,同样的环境也只需要不到1/10秒的时间。
也需要小心空模式:匹配空串的模式。比如,如果你打算用模式 '%a*' 匹配名字,你会发现到处都是名字:
i, j = string.find(";$% **#$hello13", "%a*") print(i,j) --> 1 0
有时候,使用Lua本身构造模式是很有用的。看一个例子,我们查找一个文本中行字符大于70个的行,也就是匹配一个非换行符之前有70个字符的行。我们使用字符类'[^\n]'表示非换行符的字符。所以,我们可以使用这样一个模式来满足我们的需要:重复匹配单个字符的模式70次,后面跟着一个匹配一个字符0次或多次的模式。我们不手工来写这个最终的模式,而使用函数string.rep:
pattern = string.rep("[^\n]", 70) .. "[^\n]*"
function nocase (s) s = string.gsub(s, "%a", function (c) return string.format("[%s%s]", string.lower(c), string.upper(c)) end) return s endprint(nocase("Hi there!")) --> [hH][iI] [tT][hH][eE][rR][eE]!
s1 = string.gsub(s1, "(%W)", "%%%1") s2 = string.gsub(s2, "%%", "%%%%")
"This is "great"!".
function code (s) return (string.gsub(s, "\\(.)", function (x) return string.format("\\%03d", string.byte(x)) end)) end
function decode (s) return (string.gsub(s, "\\(%d%d%d)", function (d) return "\" .. string.char(d) end)) end
s = [[follows a typical string: "This is "great"!".]] s = code(s) s = string.gsub(s, '(".-")', string.upper) s = decode(s) print(s) --> follows a typical string: "THIS IS "GREAT"!".
print(decode(string.gsub(code(s), '(".-")', string.upper)))
<command>string</command>
function code (s) return (string.gsub(s, '\\(%A)', function (x) return string.format(" \\%03d ", string.byte(x)) end)) end
function decode (s) return (string.gsub(s, '\\(%d%d%d)', string.char)) ends = [[a \emph{command} is written as \\ command\{text\}.]] s = code(s) s = string.gsub(s, "\\ (%a+){(.-)}", "<%1>%2</%1>")
print(decode(s)) --> a <emph>command</emph> is written as \command{text}.
{'a b', 'a,b', 'a,"b"c', 'hello "world"!', }
a b,"a,b"," a,""b""c", hello "world"!,
function toCSV (t) local s = "" for _,p in pairs(t) do s = s .. "," .. escapeCSV(p) end return string.sub(s, 2) -- remove first comma end
function escapeCSV (s) if string.find(s, '[,"]') then s = '"' .. string.gsub(s, '"', '""') .. '"' end return s end
"hello""hello", "",""
我们可以多次调用gsub来处理这些情况,但是对于这个任务使用传统的循环(在每个域上循环)来处理更有效。循环体的主要任务是查找下一个逗号;并将域的内容存放到一个表中。对于每一个域,我们循环查找封闭的引号。循环内使用模式 ' "("?) ' 来查找一个域的封闭的引号:如果一个引号后跟着一个引号,第二个引号将被捕获并赋给一个变量c,意味着这仍然不是一个封闭的引号。
function fromCSV (s) s = s .. ',' -- ending comma local t = {} -- table to collect fields local fieldstart = 1 repeat -- next field is quoted? (start with `"'?) if string.find(s, '^"', fieldstart) then local a, c local i = fieldstart repeat -- find closing quote a, i, c = string.find(s, '"("?)', i+1) until c ~= '"' -- quote not followed by quote? if not i then error('unmatched "') end local f = string.sub(s, fieldstart+1, i-1) table.insert(t, (string.gsub(f, '""', '"'))) fieldstart = string.find(s, ',', i) + 1 else -- unquoted; find next comma local nexti = string.find(s, ',', fieldstart) table.insert(t, string.sub(s, fieldstart, nexti-1)) fieldstart = nexti + 1 end until fieldstart > string.len(s) return t endt = fromCSV('"hello "" hello", "",""') for i, s in ipairs(t) do print(i, s) end --> 1 hello " hello --> 2 "" --> 3