概念
Lua中的匹配模式是通过常规的字符串(regular strings)来描述的,它有些类似正则表达式,在Lua代码中应用比较广泛。Lua中并没有实现正则表达式这种强大的功能,而是通过匹配模式来支持这类功能,主要原因是因为要实现完整规范的正则表达式功能一般需要几千行代码,所以出于程序代码量的考虑,Lua选择了实现匹配模式,Lua中匹配模式的实现才几百行。匹配模式虽然没有正则表达式那么强大,但也可以满足基本的应用场景,比较实用。
首先举一个简单的匹配模式例子:
例1:
str = “today is 2021/05/22”
date_reg = “%d%d%d%d/%d%d/%d%d”
print(string.match(str, date_re))
--输出: 2021/05/22
上述例子中date_reg字符串就称为匹配模式,其中%d、/被称作字符类(Character Class),通过匹配模式就能从待匹配的字符串中找出匹配的子字符串。
匹配模式是由一系列的字符类组成的,Lua中字符类如下所示:
- x,表示字符x,特殊字符(magic characters)除外
- .,表示任何字符
- %a,表示任何字母
- %c,表示任何控制字符
- %d,表示任何数字
- %g,表示任何可打印的字符,除了空格符
- %l,表示任何小写字母
- %p,表示任何标点符号
- %s,表示任何空白符
- %u,表示任何大写字母
- %w,表示任何字母/数字
- %x,表示任何十六进制
- %x,表示特殊字符(magic characters)
- [set],表示set字符集中的一个,例如[%w_]表示任何字母/数字或者下划线
- [^set],表示set字符集以外的字符,例如[^%s]表示任何非空白字符
- 上述字符类使用大写的时候,表示相反的含义,例如:%A表示非字母
上文提到的特殊字符(magic characters)包括:
- *,单个字符后面跟一个’*’,将匹配零个或多个该类的字符。
- +,单个字符后面跟一个’+’,将匹配一个或者多个该类的字符。
- -,单个字符后面跟一个’-‘,将匹配零个或多个该类的字符,和’*’不同的是它总是匹配尽可能短的串。
- ‘?’,单个字符后面跟一个’?’,将匹配零个或一个该类的字符。
- %n,其中1<=n<=9,匹配一个等于n号捕获物的子串。
- %bxy,其中x,y代表两个字符,从左开始每遇见一个x就加1,遇见一个y就减1,结果为0则结束,%bxy就表示从开始到结束的字符串。
- %f[set],边界模式,主要用来处理边界情况,匹配set字符集中某个字符,且前一个字符不是在该字符集中,前一个字符如果是字符串的开始字符则相当于一个空字符’\0’。
- ^,&,在匹配模式开头加^表示将锚定从字符串开始处做匹配,在匹配模式最后加$表示将锚定到字符串的结尾。如果这两个字符出现在其他位置,则没有特殊含义。
- (),在匹配模式内部用小括号()括起来表示一个子模式,这些子模式就是上文提到的捕获物。捕获物以他们左括号出现的次序来编号,例如:对于匹配模式”(a*(.)%w(%s))”,”a*(.)%w(%s)”匹配到的字符串放在第一个捕获物%1中,”(.)”匹配到的字符串放在第二个捕获物%2中,”(%s)”匹配到的字符串放在第三个捕获物%3中。空的捕获物()将捕获到当前字符串位置,例如,将匹配模式”()aa()”作用到”flaaap”上,将返回3和5。
匹配模式主要应用在string.find,string.gmatch,string.gsub,string.match这几个函数中,本文也是通过这几个函数来对匹配模式进行介绍的。
string.find
find的函数原型为string.find (s, pattern [, init [, plain]]),根据匹配模式返回匹配上的字符串的起始和结束位置,init表示开始匹配位置,plain如果为true,则匹配pattern原本的字符串,此时pattern不是匹配模式,仅仅是普通的字符串。
例2:
s = "%w+flaaap abc bcd"
print(s:find("%w+"))
print(s:find("%w+", 1, false))
print(s:find("%w+", 1, false))
print(s:find("%w+", 5, false)) --起点位置为5
print(s:find("%w+", 1, true)) --plain模式
输出:
2 2
2 2
2 2
5 9
1 3
在实际应用中,不仅可以使用string.find返回的索引值,还可以用这个函数判断一个字符串是否存在,从而做出相应的处理。
例3:
if string.find(str, pattern) then
…
end
例4:
local status = string.find(str, "LOGIN") and "login" or "logout"
string.gmatch
gmatch的函数原型是string.gmatch (s, pattern),它的返回值是一个函数,每次调用这个函数返回一个符合匹配模式pattern描述的子串,如果没有找到符合条件的子串,则该函数返回nil。实际应用如下:
例5:
ver_part = {}
for ver1, ver2, ver3, ver4 in string.gmatch(version, “(%d+).(%d+).(%d+).(%d+)”) do
ver_part = {ver1, ver2, ver3, ver4}
end
string.gsub
gsub的函数原型是string.gsub (s, pattern, repl [, n]),相比较其他三个函数,这个函数功能要复杂些,它的作用为根据匹配模式pattern在s中找出符合的子串,然后使用repl来替换上述字串(repl可以为字符串,表或者函数),其中n为可选参数,表示执行多少次。例如:
例6:
x = string.gsub("hello world", "(%w+)", "%1 %1")
--> x="hello hello world world"
这个例子中repl为字符串”%1 %1”,这里的%1就是上述特殊字符(magic characters)中所述捕获物%1,匹配模式”(%w+)”只有一个捕获物%w+编号为%1,在第一次执行中匹配命中hello,所以%1代表hello,第一次替换结果为hello hello world,在第二次执行中匹配命中world,所以%2代表world,第二次替换结果为hello hello world world。从这个例子也可以看出来,这个函数的执行过程是逐次部分替换原来字符串的。
例7:
x = string.gsub("hello world", "%w+", "%0 %0", 1)
--> x="hello hello world"
这个例子中%0表示整个%w+匹配到的字符串,且只执行一次匹配替换过程。所以匹配替换结果为hello hello world
例8:
x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1")
--> x="world hello Lua from"
本例中存在两个捕获物,分别为左边的(%w+)编号为%1,右边的(%w+)编号为%2,因为字符类%s表示空格,所以匹配模式"(%w+)%s*(%w+)"首次命中hello world且%1为hello,%2为world,然后分别替换原来的字符串,故第一次匹配替换结果为world hello。第二次命中from Lua且%1为from,%2为world,然后替换原来的字符串,故第二次匹配替换结果为world hello Lua from。
例9:
x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv)
--> x="home = /home/roberto, user = roberto"
前面几个例子repl都是字符串,这个例子repl是函数,第一次匹配替换时,把(%w+)所代表的字符串HOME传入os.getenv函数,该函数返回值替换原字符串的$HOME,同理第二次匹配替换时,把(%w+)所代表的字符串USER传入os.getenv函数,该函数返回值替换原字符串的$USER。
例10:
x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s)
return load(s)()
end)
--> x="4+5 = 9"
这个例子中repl也是函数,匹配替换时,把(.-)所代表的字符串return 4+5传入函数中,经过计算返回结果9,然后替换原字符串中的$return 4+5$,所以结果为x="4+5 = 9"
例11:
local t = {name="lua", version="5.3"}
x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t)
--> x="lua-5.3.tar.gz"
最后这个例子repl是表,在匹配替换中,拿key去索引表中对应的值。第一次匹配替换时,(%w+)匹配到了key值name,取到表中对应的值为lua,然后替换原字符串中的$name,第二次匹配替换时,(%w+)匹配到了key值version,取到表中对应的值为5.3,然后替换原字符串中的$version。所以最终结果为x="lua-5.3.tar.gz"。
string.match
string.match函数原型是match (s, pattern [, init]),它的外观看起来和string.gmatch类似,但是实际上用法相差比较大,string.match直接返回s中满足匹配模式pattern的子串,可选参数init表示开始匹配的位置。例如:
例12:
local number = string.match(str, "累计对玩家造成(%d+)伤害")则返回伤害的数值。
源码简析
匹配模式涉及到的概念比较多,有的地方需要结合源码才能更好理解。匹配模式这部分的源码主要在lstrlib.c这个文件中。首先,通过luaL_newlib对string.find,string.gsub,string.gmatch和string.match这四个函数进行注册。
其中strlib数组定义如下:
这四个函数的实现并不复杂,源码实现中注释也比较多,方便理解。实现中用到了很多c语言的库函数,核心逻辑都是用到了match函数。下文主要结合源码举几个例子解释一下匹配模式中容易忽略或者不好理解的地方。
(1)如上述例2,例3和例4所示,find常用在查找字符串是否存在或返回被查找字符串在原字符串中的起始和结束位置,通过源码就可以发现还有一种应用就是可以返回捕获物:
例13:
date = "2021/5/28"
_, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
print(d, m, y) --> 2021 5 28
(2)官方文档的边界模式(frontier)看起来可能有些不好理解,看看源码可以帮助理解:
从上面这段代码可以看出来,边界模式(frontier)主要就是为了忽略或者跳过匹配模式中指定的字符去匹配%f[set]之后的字符串。例如打印出字符串中大写子串这个功能:
可能写成这样的实现代码:
string.gsub ("the FUNNY brown min", "%u+", print)
输出:
FUNNY
但是对于以下这种待匹配的字符串就不能满足了,错误的多匹配了一个BRO:
string.gsub ("the FUNNY BROwn min", "%u+", print)
输出:
FUNNY
BRO
接着改一下匹配模式:
string.gsub ("the FUNNY BROwn min", "%u+%A", print)
输出:
FUNNY
但是对于以下这种又不适用了,多匹配了一个OWN:
string.gsub ("the FUNNY brOWN min", "%u+%A", print)
输出:
FUNNY
OWN
或者这种也不适用,多匹配了一个.:
string.gsub ("the FUNNY. brown min", "%u+%A", print)
输出:
FUNNY.
继续优化匹配模式为"%A%u+%A",但是前后多匹配了一个空格:
string.gsub ("the FUNNY brOWN MIn jumps", "%A%u+%A", print)
输出:
FUNNY
改成这样错误更明显,前后多匹配了一个'('和')':
string.gsub ("the (FUNNY) brOWN MIn jumps", "%A%u+%A", print)
输出:
(FUNNY)
又或者这种情况,THE和JUMPS满足要求,但是没有打印出来:
string.gsub ("THE (FUNNY) brOWN MIn JUMPS", "%A%u+%A", print)
输出:
(FUNNY)
上述的例子说明优化后的匹配模式还是没能完成查找大写字符串的任务,但是这种情况恰好就是边界模式(frontier)擅长处理的,使用%f[%a]跳过%u+前面的字符,使用%f[%A]丢掉%u+后面的字符,输出结果完全正确:
string.gsub ("THE (FUNNY) brOWN MIx JUMPS", "%f[%a]%u+%f[%A]", print)
输出:
THE
FUNNY
JUMPS
(3)通过看源码会发现find的实现和string.match的实现差不多,甚至是在同一个函数str_find_aux中实现的,该函数通过find这个标记来表明处理的string.find还是string.match函数。
(4)特殊字符(magic characters)中的*和-的都是表示匹配零个或多个该类的字符,通过源码可以更加清晰的看出它们的区别。
-- 期望将<html>xxx</html>中的xxx替换成lua
-- 贪婪匹配,替换了整个字符串
print(string.gsub("<html>hello</html><html>world</html>","<html>.+</html>","<html>lua</html>"))
--> <html>lua</html>
-- 非贪婪匹配,结果符合预期
print(string.gsub("<html>hello</html><html>world</html>","<html>.-</html>","<html>lua</html>"))
--> <html>lua</html><html>lua</html>
当为'*'时调用的函数如下,首先就会先把搜索下标移到最大匹配可能的位置,从而匹配更长的满足匹配要求的字符串,所以是一种贪婪匹配模式。
当为'-'时调用的函数如下,只要匹配到满足要求的字符串即返回,所以是一种非贪婪匹配模式。
匹配模式相关源码中还有很多高效有趣的实现,这里就不一一列举了,感兴趣的小伙伴可以继续参考Lua源码。
参考资料
http://www.lua.org/manual/5.3/manual.html#6.4.1
https://juejin.cn/post/6874512568913199117
http://lua-users.org/wiki/FrontierPattern
https://www.cnblogs.com/zhong-dev/p/4044568.html
https://www.coder.work/article/6850306