加入收藏 |
设为首页 |
会员中心 | 我要投稿
|
李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
Lua模式匹配
发布时间:2020-12-14 22:03:32 所属栏目:大数据 来源:网络整理
导读:已经拿Lua用了快两年的时间了,但是每次用到字符串的模式匹配的时候就总要去翻看Lua官网的说明,网上也没有一个比较详细的说明,也有好多朋友都向我询问这块的内容,其实这块的难点有三: 一个是对Lua的正则表达式不熟悉; 另一个是对Lua中string库提供的几
已经拿Lua用了快两年的时间了,但是每次用到字符串的模式匹配的时候就总要去翻看Lua官网的说明,网上也没有一个比较详细的说明,也有好多朋友都向我询问这块的内容,其实这块的难点有三:
这里我总结一下。
先从Lua内置string库提供的几个大家不熟悉的函数开始(基于Lua5.1,Lua5.2基本没有变化)。
Lua内置字符串库用到模式的地方有4个函数,它们分别是:
string.find()
string.match()
string.gmatch()
1、string.find(s,pattern,start,plain)
这个函数的功能是查找字符串 s 中的指定的模式 pattern。
如果找到了一个模式的匹配,就返回找到的模式在 s 中的起点和终点;否则返回 nil。这里需要注意的是,它只会返回找到的第一个匹配的位置,所以找到了的返回值其实是两个数字,匹配的起点、匹配的终点。
第三个参数是个数字,它是可选的,start 指定在 s 中查找开始的位置,默认是 1,start可以是负数,-1 代表从最后一个字符开始,-2 代表倒数第二个字符开始。当然,最后都是到最后一个字符结束,所以如果你指定位置从最后一个字符开始,那么就只会查找这一个字符。
第四个参数是个 bool 值,它指明第二个参数 pattern 中是否使用特殊字符,如果第四个参数指明为 true,那么就意味着第二个参数 pattern 中的那些特殊字符(这些字符有?^$*+?.([%-?,定义在Lua源码 lstrlib.c 中)都被当作正常字符进行处理,也就是一个简单的字符串匹配,而不过所谓的模式匹配,也就是不动用正则表达式的匹配。相反,false 就意味着 pattern 采用特殊字符处理。这样说也不太明了,举个例子就明白了,不过要涉及到一个Lua模式中特殊的字符,如果这里还是不明白,看了后面我关于Lua正则表达式的介绍应该就能明白。
比如:
1
2
3
|
local s =?
"am+df"
print(string.find(s,?
"m+"
,1,?
false
))??? -- 2??? 2
true
))??? -- 2??? 3
|
其中字符?+?在 Lua 正则表达式中的意思是匹配在它之前的那个字符一次或者多次,也就是说?m+?在正则表达式里会去匹配 m,mm,mmm ……。所以当 string.find 第四个参数为 false 的时候,就只能在字符串 s 中找到?m?这个字母是匹配的,那么返回的结果就是 2 ? ?2。
而当第四个参数为 true 的时候,?+?被当作正常字符,那么查找的匹配就是?m+?这个字符串,那么找到的位置就是 2 ? ? 3。
如果你不传第四个参数,就跟 false 是一个意思。
上面把 find 函数做了一个简单的介绍,但是这个函数的行为并非总是这样,为什么呢?这就是我文章开头提到的Lua的捕获也会被杂糅到这些string的库函数里。
没有办法,只得先介绍一下所谓的捕获是个什么概念。
上面 find 函数的第二个参数我们都明白是一个模式,可以理解为一般的正则匹配中的正则表达式,而Lua为这个模式增加了一个新的功能,也就是所谓的捕获,在一个模式串中,我们可以用小括号()来标明一些我们想要保存的匹配,而这个小括号中的内容依然是模式串,也就是说我们只不过是把模式中一些我们想要的特殊字符保留下来供后面使用。比如上面那个例子中的模式串是?m+?,如果我想要把跟m+?匹配的字符串捕获出来,也就是保存下来,我可以用一个小括号把它括起来,而 find 函数除了上面说到的行为外,也就是除了返回查找到 pattern 的起止位置外,还会返回所有要求捕获的字符串,像这样:
|
"(m+)"
))??? -- 2??? 2??? m
|
如果你想要捕获更多的内容,只需要用小括号把它括起来就好了,比如这样:
1
2
3
|
local s =?
"am+df"
"((m+))"
false
))??? -- 2??? 2??? m??? m
"(((m+)))"
))??? -- 2??? 2??? m??? m??? m
|
关于捕获还有一点需要说明的,就是捕获只会在模式能够匹配成功的时候才会跟着 string 的函数进行返回,比如下面这个,我想捕获字母?a?,但事实上这个模式根本无法匹配到,所以肯定是无法返回的:
另外捕获返回的顺序,是依照左小括号的位置来定的,比如上面那个捕获了3个?m?的例子,第一个?m其实是最外层的小括号捕获到的。为什么要提到捕获的顺序呢?因为我们可以使用?%n?来取得第n个捕获的字符串,至于取得对应的捕获有什么用处呢?这个在后面会介绍到。
一个空的捕获,也就是小括号里面什么内容也没有,它会返回当前字符串的比较操作进行到的位置,比如
local s = ”am+df“
"()(m+)()"
))??? -- 2??? 2??? 2??? m??? 3
有一点也必须要提一下,就是在Lua5.1的源码当中,捕获字符串的数量是有限制的,默认是32个,也就是说你添加的小括号不能无限加,最多加32个。如果捕获超过限制,当然会报错了,比如:
2
"()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()"
))??? -- 捕获33个
|
当然你可以通过修改Lua的源码来调整你想要保存的捕获数量,这个数量定义在 luaconf.h 文件中:
一般来说,对于使用,分析基本到此了,但是对于 Lua,因为源码简单,而且优美,又是拿C语言写的,心痒难耐,必须要了解一下源码才解恨。
Lua内置库的加载方式就不说了,在各个大神的文章里都可以看到,我们直接来看 string.find() 这个函数,函数在 lstrlib.c 文件里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
static ?
int ?
str_find (lua_State *L) {
??
return ?
str_find_aux(L,1);
}
str_find_aux (lua_State *L,?
find) {
??
size_t ?
l1,l2;
const ?
char ?
*s = luaL_checklstring(L,&l1);
*p = luaL_checklstring(L,2,&l2);
ptrdiff_t ?
init = posrelat(luaL_optinteger(L,3,1),l1) - 1;
if ?
(init < 0) init = 0;
else ?
((
size_t
)(init) > l1) init = (
ptrdiff_t
)l1;
(find && (lua_toboolean(L,4) ||??
??????
strpbrk
(p,SPECIALS) == NULL)) {??
????
????
*s2 = lmemfind(s+init,l1-init,p,l2);
(s2) {
??????
lua_pushinteger(L,s2-s+1);
??????
2;
????
}
??
}
else ?
{
MatchState ms;
????
anchor = (*p ==?
'^'
) ? (p++,1) : 0;
*s1=s+init;
ms.L = L;
ms.src_init = s;
ms.src_end = s+l1;
do ?
{
*res;
ms.level = 0;
((res=match(&ms,s1,p)) != NULL) {
????????
(find) {
??????????
/* start */
/* end */
??????????
push_captures(&ms,NULL,0) + 2;
????????
}
else
}
}?
while ?
(s1++ < ms.src_end && !anchor);
}
lua_pushnil(L);??
1;
}
|
这个函数初步看起来还是比较长的,但是仔细分析一下就发现其实是很简单的。前面那 6 行,就是接收前 3 个参数罢了,只不过处理了一下那个查找起始点参数,防止了超出字符串长度。最关键的地方就是紧接着的 if else 逻辑,find 是传进来的参数,对于 string.find 来说就是1,所以不用管它,认为它一直是真就 OK 了,既然提到这里了,那么是不是还有别的地方也会调用这个函数原型的,bingo!我们搜索一下就会发现,其实 string.match() 函数其实也是调用这个函数原型的,而它的 find 参数就是传递的 0 。哈哈,难道 string.match 函数其实跟 string.find 函数是一样的?
6
str_match (lua_State *L) {
str_find (lua_State *L) {
这个留到介绍 string.match 函数的时候再说。拉回来,继续谈这个 if else 逻辑,if 的判断条件其实就是看你调用 string.find 的第四个参数,如果第四个参数传递了 true,也就是我上面说的,不使用特殊字符模式,或者是模式中压根就没有特殊字符,那个?SPECIALS?宏同样定义在这个文件中:

如果没有这些字符或者是不对这些字符特殊处理,那么就是一个简单的字符串匹配,调用?lmemfind()函数,如果找到了,就返回了匹配到的起止位置。
既然如此,那么 else 里就好理解了,它就是使用特殊字符进行匹配的处理,这里的关键函数是match(),它处理字符串和模式进行匹配,并进行了捕获,这个留到介绍模式的时候再接着说。最后如果匹配到了,那么仍然返回匹配起止点,注意,这里多了一个操作,就是把捕获到的字符串也压入了栈。所以我们调用并捕获的时候才会有后面那些捕获的字符串。
这么看来还是挺好理解的嘛。在好奇心的趋势下,我非常感兴趣,Lua的那个?lmemfind()?函数是如何进行字符串匹配的,难道是传说中的 KMP又或者是BM算法?如果不熟悉这两种算法的童鞋,可以看看阮一峰的这两篇文章:
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
,
http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
怀着一点小激动,我点开了 lmemfind() :
20
|
static?
*lmemfind (
*s1,
???????????????????????????????
*s2,monospace!important; font-size:1em!important; min-height:inherit!important; background:none!important">l2) {
(l2 == 0)?
s1;??
(l2 > l1)?
NULL;??
{
*init;??
l2--;??
l1 = l1-l2;??
(l1 > 0 && (init = (
*)
memchr
(s1,*s2,l1)) != NULL) {
init++;???
(
memcmp
(init,s2+1,l2) == 0)
init-1;
{??
l1 -= init-s1;
s1 = init;
}
}
/* not found */
}
总的来说,这个比较的方法还是中规中矩的,从头开始查找匹配串的第一个字符,只不过用的是memchr?函数,找到了之后用?memcmp?函数来比较两个字符串是否是相同的,如果不相同就跳过检查了的字符继续。相比那些复杂的字符串匹配算法,这个既简单又可爱,赞一个:),memcmp?函数的执行自然比 str 系列的比较要快一些,因为不用一直检查 ‘ ’ 字符,关于?memcmp?函数的做法,这里有一篇文章,虽然是说他的优化,但是看他的代码也能大致了解?memcmp?的做法:http://blog.chinaunix.net/uid-25627207-id-3556923.html
2、string.match(s,start)
这个函数的功能是在字符串 s 中查找指定的模式 pattern,并返回模式 pattern 中指定的第一个捕获。
第三个参数指明查找的起始位置,默认为1。
相比 string.find 函数来说,string.match 要简单的多了,它不需要你再选择是否采用特殊字符,它必须要采用。pattern 中的内容跟 string.find 一样,都是一个Lua的模式,跟 string.find 不同的地方在于,它返回的不在是匹配到的起止位置,而是返回 pattern 中指定的第一个捕获,如果 pattern 中没有指明捕获,那么它会返回整个 pattern 的匹配结果,当然,没有匹配到依然返回 nil。
在介绍 string.find 函数的时候提到过,Lua 源码中 string.match 调用的函数其实跟 string.find 调用的函数是相同的,都是?str_find_aux(lua_State *L,int find)?函数,唯一不同的地方在于 string.match 调用时 find 参数传递的是 0,这样就不会进入?str_find_aux()?里简单匹配的分支,直接进行模式匹配。
3、string.gmatch(s,pattern)
上面介绍的两个函数,无论是 string.find 还是 string.match 函数,都是发现和模式相匹配的串后就停下来了,返回对应的内容,而经常我们会有在一个字符串中找到所有跟模式相匹配的串的需求,string.gmatch() 函数就能够满足这个需求。
string.gmatch() 函数可以被当作迭代器进行调用,然后获得所有跟模式相匹配的串,比如Lua官网给出的例子:
|
s =?
"hello world from Lua"
for ?
w?
in ?
string.gmatch(s,monospace!important; font-size:1em!important; min-height:inherit!important; color:blue!important; background:none!important">"%a+"
)?
do
????
print(w)
end
--[[
hello
world
from
Lua
]]
|
至于?%a+?的意义嘛,在 string.find() 的介绍里提到过字符?+?的用法,至于?%a?嘛,它是匹配所有的字母。这里需要注意的是,字符串 s 里由 4 个单词,用 3 个空格进行了分隔,所以调用一次 string.gmatch(s,"%a+"),只会匹配 s 中的第一个单词,因为遇到空格匹配就失败了。
string.gmatch() 函数的用法基本也就是在循环里当作迭代器用了,我还真没发现有别的用法。
一个唯一需要注意的地方,就是特殊字符?^?在 string.gmatch 函数中的用法跟别处是不同的,在其他的函数中,^?的用法是放在一个模式的前面,那么这个模式必须从字符串的最开头开始匹配,如果匹配失败则不会继续去和后面的字符匹配了,什么意思呢,用前面的例子:
"(m+)"
))??? -- 2??? 2??? m
"^(m+)"
第二个匹配,因为在模式前面增加了?^?,所以会从字符串 s 的最开始就进行匹配,也就是从字母?a开始匹配,a?当然无法和?(m+)?匹配成功了,所以直接就返回 nil 了。这个处理在上面讲 string.find 的时候源码函数?str_find_aux()?里 else 分支模式匹配里可以看到,有专门处理?^?字符的代码。
那么在 string.gmatch 函数中,如果把?^?放在模式的前面的意思什么呢?它的意思是不进行匹配了,也就是直接返回,注意不是返回 nil,而是函数直接返回,栈上没有任何返回值。
老样子,虽然可以想象得到 string.gmatch() 的实现应该跟上面的差不多,但还是看一眼源码比较保险:
前面那两行是返回Lua迭代器所需求的状态和迭代函数,不用去管它。让我们来看一下?gmatch_aux (lua_State *L)?函数,刨去为了迭代器做处理之后,就和 string.match() 函数实现没有什么区别了,最后都调用 match() 函数进行模式匹配。不同的地方就是上面说的字符 ^ 的处理这里是没有的。
4、string.gsub(s,rep,n)
这个函数跟 string.gmatch() 一样,也带一个?g,可以想象得到,这个函数也会获得所有的匹配字符串,而不像 string.find() 和 string.match() 一样,碰到一个就结束了。确实是这样。这个函数的作用是在字符串 s 中查找与模式 pattern 相匹配的所有串,然后用 rep 参数产生的字符串进行替换,你可能要问,为什么是 rep 产生的串,而不是 rep 自己呢?因为这里的 rep 除了可以是一个字符串外,还可以是一个函数,甚至可以是一个table。
当 rep 是一个字符串的时候,一般来说是当作一个普通的字符串来处理,也就是直接用这个字符串来替换匹配到的串,但是这里会特殊的处理符号?%,%?后面接数字 1-9 的时候,也就是用前面模式捕获的序号 1-9 对应的字符串来替换模式匹配的内容,这样说比较绕,还是看例子:
4
"am+dmf"
print(string.gsub(s,monospace!important; font-size:1em!important; min-height:inherit!important; color:blue!important; background:none!important">"()(m+)"
"%1"
))???? -- a2+d5f???? 2
"%2"
))????? -- am+dmf???? 2
"%3"
))??? -- error: invalid capture index
|
上面我们用的模式是?()(m+),这个模式会有2个捕获,分别是字符串当前的位置以及?m+,string.gsub() 匹配到的第一个地方是?
am+dmf
,这个时候两个捕获分别是?2 ,m,那么?%1?也就是第一个捕获,也就是?2,替换后的串为?a2+dmf,接着又匹配到第二个地方?
am+dmf
,这里的两个捕获分别是?5,m,那么?%1?指向的第一个捕获是?5,替换后的串为?a2+d5f,这就是结果显示的内容。后面那个数字?2?的意思是替换成功了2次。根据上面的分析就不难理解,为什么用?%2?去替换的时候字符串没有变,因为本来就是用?m?去替换?m,当然不变。另外,第三个 print() 会报错,因为只有2个捕获,而你要去使用?%3?,那么自然就没有这个捕获了。
这里可能还需要注意的地方就是?%?只会和后面紧接着的数字结合,换句话说为什么前面要说是 1-9 就是这个原因,虽然捕获可以默认达到之前说的 32 个,但是只能用前 9 个了。有一个比较特殊的是%0,它是用匹配到的串去替换,简单来说就是重复匹配到的串,比如这样:
"%0%0%0"
))??? -- ammm+dmmmf??? 2
匹配到的串是?m,用?mmm?替换了原串中的?m。
你可能要问,既然?%?被单独处理了,那么我想要用?%?去替换怎么办,只需要用?%%?就可以表示?%?自身了。比如:
"%%"
))??? -- a%+d%f??? 2
当rep是一个table的时候,每次匹配到了之后,都会用第一个捕获作为key去查询这个table,然后用table的内容来替换匹配串,如果没有指定捕获,那么,就用整个匹配串作为key去查询,如果没有查到对应key的值,或者对应的值不是字符串和数字,那么就不做替换:
10
11
12
13
14
15
16
local t1 = {
[2] =?
"hh"
[5] =?
"xx"
}
local t2 = {}
local t3 = {
[2] =?
false
}
local t4 = {
[2] = { 123 }
}
当rep是一个函数的时候,每当匹配到字符串的时候,就把模式所有的捕获按照捕获顺序作为参数传递给这个函数,如果没有指定捕获,则传递整个匹配的字符串给函数,函数的返回值如果是字符串或者是数字就替换掉匹配,如果不是则不做替换:
"am+dmf"
function ?
f1(...)
?????
print(...)???? -- 2???? m??? -- 5???? m
?????
return ?
"hh"
end
f2()
return ?
{ 123 }
end
第四个参数,用来表明,需要替换到第几个匹配为止,比如:
|
依然来看看源码是怎么写的:
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
static ?
int ?
str_gsub (lua_State *L) {
??
size_t ?
srcl;
??
const ?
char ?
*src = luaL_checklstring(L,&srcl);
*p = luaL_checkstring(L,2);
max_s = luaL_optint(L,4,srcl+1);
anchor = (*p ==?
'^'
n = 0;
??
MatchState ms;
luaL_Buffer b;
luaL_buffinit(L,&b);
ms.L = L;
ms.src_init = src;
ms.src_end = src+srcl;
while ?
(n < max_s) {
????
*e;
????
ms.level = 0;
e = match(&ms,src,p);
if ?
(e) {
??????
n++;
add_value(&ms,&b,e);
}
(e && e>src)?
src = e;??
else ?
(src < ms.src_end)
luaL_addchar(&b,*src++);
break
;
(anchor)?
;
}
luaL_addlstring(&b,ms.src_end-src);
luaL_pushresult(&b);
/* number of substitutions */
return ?
2;
}
|
可以看到它处理了符号?^?,循环进行匹配,如果匹配到了,就按照不同的类型把替换串添加进结果里,最后把所有字符压回栈上。
总的来说 string.gsub() 函数实现的效果跟我们一般意义上的替换是相同的,你可能会纳闷为什么它不叫 string.greplace ,其实我也纳闷。
上面介绍完了 4 个用到了模式的函数之后,我们再来看看Lua的模式有什么奇妙之处。
模式
让我们来看看,都有哪些特殊字符需要解释,其实这一部分在Lua的官方文档中,介绍的还是很清楚的:
首先,任何单独的字符,除了上面那些特殊字符外,都代表他们本身。注意前提是他们独立出现。
其次,Lua定义了一些集合,它们分别如下:
.?:代表任意的字符。
%a?:代表任意字母。
%c?:代表任意控制字符。
%d?:代表任意数字。
%l?:代表任意小写字母。
%p?:代表任意标点符号。
%s?:代表任意空白字符(比如空格,tab啊)。
%u?:代表任意大写字母。
%w?:代表任意字母和数字。
%x?:代表任意16进制数字。
%z?:代表任意跟0相等的字符。
%后面跟任意一个非字母和数字的字符,都代表了这个字符本身,包括上面那些特殊字符以及任何标点符号都可以用这个方式来表达。
[set]?:代表一个自定义的字符集合。你可以使用符号?-?来标识一个范围,比如 1-9,a-z 之类的。需要注意的是,上面提到的那些字符集合也可以在这个自定义的集合里用,但是你不能这么写[%a-z],这样的集合是没有意义的。
[^set]?:代表字符集合[set]的补集(补集是什么意思,我了个去,问你数学老师去)。
另外,对于上面提到的所有用?%?跟一个字母组成的集合,如果把字母大写,那么就对应那个集合的补集,比如?%S?的意思就是所有非空白字符。Lua官网还强调了一下,这里个定义跟本地的字符集有关,比如集合 [a-z] 就不一定跟 %l 是相等的。
任意一个单字符表达的集合,包括?%?加单字符表达的集合后面都可以跟4种符号,他们分别是?*?、?+、?-?、??。
*?:意思是前面的集合匹配0个或者更多字符,并且是尽量多的匹配。
+?:意思是前面的集合匹配1个或者更多字符。
-?:意思是前面的集合匹配0个或者更多字符,尽量少的匹配。
??:意思是前面的集合匹配0个或者1个。
如下:
local a =?
"ammmf"
print(string.match(a,monospace!important; font-size:1em!important; min-height:inherit!important; color:blue!important; background:none!important">"%a"
))???? -- a
"%a*"
))???? -- ammmf
))???? -- ammmf
"%a-"
))???? --
"%a?"
))??? -- a
看了上面的例子,你可能会想,那?*?和?+?或者加不加???有什么区别呢?是有区别的,因为匹配0个和匹配1个有的时候就是有没有匹配成功的关键,比如加上???就可以匹配0个,意味着即使没有对应集合的内容,也算匹配成功了,如果有捕获的话,这个时候捕获是生效的。比如:
"()c"
))???? -- nil
"()c?"
))??? -- 1
如果你现在还不知道 string.match() 是什么意思,就翻到前面去看吧。
还有一个特殊的字符需要介绍,就是?%b?后面跟两个不同的字符xy,它的意思是匹配从x开始,到y结束的字符串,而且要求这个字符串里x和y的数量要相同。比如?%b()?就是匹配正常的小括号,如下:
"aaabb"
"%bab"
))??? -- aabb
最后,我在介绍 string.gmatch 的时候介绍过字符?^?的用法,它放在模式的首部,意思是从原串的首部就开始匹配,这里还有一个特殊字符跟它的用法类似,它就是?$?字符,这个字符放在模式的末尾,意思是从原串的尾部开始匹配。在其他位置就跟?^?一样,也没有意义。
捕获
捕获的意思在介绍 string.find 的时候已经详细介绍过了,这里再提一笔,捕获是在模式中,用小括号括起来的子模式,它在匹配发生的时候截取小括号内模式匹配到的字符串,然后保存下来,默认最多保存 32 个,可以在Lua源码中修改保存的数量。另外捕获的顺序是按照小括号左括号的位置来定的。至于捕获如何使用,请参看我上面介绍的4个使用了模式的函数的具体用法。
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!