-
一个是对Lua的正则表达式不熟悉;
-
另一个是对Lua中string库提供的几个函数的用法不熟悉;
-
还有一点是Lua的string库提出了一个新的概念,叫做捕获,其实也不算什么新概念,只是和函数调用杂糅在一起行为就不好理解罢了。
先从Lua内置string库提供的几个大家不熟悉的函数开始(基于Lua5.1,Lua5.2基本没有变化)。
-
string.find()
-
string.match()
-
string.gmatch()
-
string.gsub()
如果找到了一个模式的匹配,就返回找到的模式在 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
print(string.find(s,
"m+"
, 1,
true
)) -- 2 3
|
而当第四个参数为 true 的时候, + 被当作正常字符,那么查找的匹配就是 m+ 这个字符串,那么找到的位置就是 2 3。
如果你不传第四个参数,就跟 false 是一个意思。
上面把 find 函数做了一个简单的介绍,但是这个函数的行为并非总是这样,为什么呢?这就是我文章开头提到的Lua的捕获也会被杂糅到这些string的库函数里。
没有办法,只得先介绍一下所谓的捕获是个什么概念。
上面 find 函数的第二个参数我们都明白是一个模式,可以理解为一般的正则匹配中的正则表达式,而Lua为这个模式增加了一个新的功能,也就是所谓的捕获,在一个模式串中,我们可以用小括号()来标明一些我们想要保存的匹配,而这个小括号中的内容依然是模式串,也就是说我们只不过是把模式中一些我们想要的特殊字符保留下来供后面使用。比如上面那个例子中的模式串是 m+ ,如果我想要把跟m+ 匹配的字符串捕获出来,也就是保存下来,我可以用一个小括号把它括起来,而 find 函数除了上面说到的行为外,也就是除了返回查找到 pattern 的起止位置外,还会返回所有要求捕获的字符串,像这样:
1
2
|
local s =
"am+df"
print(string.find(s,
"(m+)"
, 1,
false
)) -- 2 2 m
|
1
2
3
|
local s =
"am+df"
print(string.find(s,
"((m+))"
, 1,
false
)) -- 2 2 m m
print(string.find(s,
"(((m+)))"
, 1,
false
)) -- 2 2 m m m
|
1
2
|
local s =
"am+df"
print(string.find(s,
"(m+)(a)"
, 1,
false
)) -- nil
|
一个空的捕获,也就是小括号里面什么内容也没有,它会返回当前字符串的比较操作进行到的位置,比如
1
2
|
local s = ”am+df“
print(string.find(s,
"()(m+)()"
, 1,
false
)) -- 2 2 2 m 3
|
1
2
|
local s = ”am+df“
print(string.find(s,
"()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()"
, 1,
false
)) -- 捕获33个
|
当然你可以通过修改Lua的源码来调整你想要保存的捕获数量,这个数量定义在 luaconf.h 文件中:
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);
}
static
int
str_find_aux (lua_State *L,
int
find) {
size_t
l1, l2;
const
char
*s = luaL_checklstring(L, 1, &l1);
const
char
*p = luaL_checklstring(L, 2, &l2);
ptrdiff_t
init = posrelat(luaL_optinteger(L, 3, 1), l1) - 1;
if
(init < 0) init = 0;
else
if
((
size_t
)(init) > l1) init = (
ptrdiff_t
)l1;
if
(find && (lua_toboolean(L, 4) ||
/* explicit request? */
strpbrk
(p, SPECIALS) == NULL)) {
/* or no special characters? */
/* do a plain search */
const
char
*s2 = lmemfind(s+init, l1-init, p, l2);
if
(s2) {
lua_pushinteger(L, s2-s+1);
lua_pushinteger(L, s2-s+l2);
return
2;
}
}
else
{
MatchState ms;
int
anchor = (*p ==
'^'
) ? (p++, 1) : 0;
const
char
*s1=s+init;
ms.L = L;
ms.src_init = s;
ms.src_end = s+l1;
do
{
const
char
*res;
ms.level = 0;
if
((res=match(&ms, s1, p)) != NULL) {
if
(find) {
lua_pushinteger(L, s1-s+1);
/* start */
lua_pushinteger(L, res-s);
/* end */
return
push_captures(&ms, NULL, 0) + 2;
}
else
return
push_captures(&ms, s1, res);
}
}
while
(s1++ < ms.src_end && !anchor);
}
lua_pushnil(L);
/* not found */
return
1;
}
|
1
2
3
4
5
6
|
static
int
str_match (lua_State *L) {
return
str_find_aux(L, 0);
}
static
int
str_find (lua_State *L) {
return
str_find_aux(L, 1);
}
|
如果没有这些字符或者是不对这些字符特殊处理,那么就是一个简单的字符串匹配,调用 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static
const
char
*lmemfind (
const
char
*s1,
size_t
l1,
const
char
*s2,
size_t
l2) {
if
(l2 == 0)
return
s1;
/* empty strings are everywhere */
else
if
(l2 > l1)
return
NULL;
/* avoids a negative `l1' */
else
{
const
char
*init;
/* to search for a `*s2' inside `s1' */
l2--;
/* 1st char will be checked by `memchr' */
l1 = l1-l2;
/* `s2' cannot be found after that */
while
(l1 > 0 && (init = (
const
char
*)
memchr
(s1, *s2, l1)) != NULL) {
init++;
/* 1st char is already checked */
if
(
memcmp
(init, s2+1, l2) == 0)
return
init-1;
else
{
/* correct `l1' and `s1' to try again */
l1 -= init-s1;
s1 = init;
}
}
return
NULL;
/* not found */
}
}
|
2、string.match(s, pattern, start)
这个函数的功能是在字符串 s 中查找指定的模式 pattern,并返回模式 pattern 中指定的第一个捕获。
第三个参数指明查找的起始位置,默认为1。
相比 string.find 函数来说,string.match 要简单的多了,它不需要你再选择是否采用特殊字符,它必须要采用。pattern 中的内容跟 string.find 一样,都是一个Lua的模式,跟 string.find 不同的地方在于,它返回的不在是匹配到的起止位置,而是返回 pattern 中指定的第一个捕获,如果 pattern 中没有指明捕获,那么它会返回整个 pattern 的匹配结果,当然,没有匹配到依然返回 nil。
3、string.gmatch(s, pattern)
上面介绍的两个函数,无论是 string.find 还是 string.match 函数,都是发现和模式相匹配的串后就停下来了,返回对应的内容,而经常我们会有在一个字符串中找到所有跟模式相匹配的串的需求,string.gmatch() 函数就能够满足这个需求。
string.gmatch() 函数可以被当作迭代器进行调用,然后获得所有跟模式相匹配的串,比如Lua官网给出的例子:
1
2
3
4
5
6
7
8
9
10
|
s =
"hello world from Lua"
for
w
in
string.gmatch(s,
"%a+"
)
do
print(w)
end
--[[
hello
world
from
Lua
]]
|
string.gmatch() 函数的用法基本也就是在循环里当作迭代器用了,我还真没发现有别的用法。
一个唯一需要注意的地方,就是特殊字符 ^ 在 string.gmatch 函数中的用法跟别处是不同的,在其他的函数中,^ 的用法是放在一个模式的前面,那么这个模式必须从字符串的最开头开始匹配,如果匹配失败则不会继续去和后面的字符匹配了,什么意思呢,用前面的例子:
1
2
3
|
local s =
"am+df"
print(string.find(s,
"(m+)"
, 1,
false
)) -- 2 2 m
print(string.find(s,
"^(m+)"
, 1,
false
)) -- nil
|
那么在 string.gmatch 函数中,如果把 ^ 放在模式的前面的意思什么呢?它的意思是不进行匹配了,也就是直接返回,注意不是返回 nil,而是函数直接返回,栈上没有任何返回值。
老样子,虽然可以想象得到 string.gmatch() 的实现应该跟上面的差不多,但还是看一眼源码比较保险:
4、string.gsub(s, pattern, rep, n)
这个函数跟 string.gmatch() 一样,也带一个 g,可以想象得到,这个函数也会获得所有的匹配字符串,而不像 string.find() 和 string.match() 一样,碰到一个就结束了。确实是这样。这个函数的作用是在字符串 s 中查找与模式 pattern 相匹配的所有串,然后用 rep 参数产生的字符串进行替换,你可能要问,为什么是 rep 产生的串,而不是 rep 自己呢?因为这里的 rep 除了可以是一个字符串外,还可以是一个函数,甚至可以是一个table。
当 rep 是一个字符串的时候,一般来说是当作一个普通的字符串来处理,也就是直接用这个字符串来替换匹配到的串,但是这里会特殊的处理符号 %,% 后面接数字 1-9 的时候,也就是用前面模式捕获的序号 1-9 对应的字符串来替换模式匹配的内容,这样说比较绕,还是看例子:
1
2
3
4
|
local s =
"am+dmf"
print(string.gsub(s,
"()(m+)"
,
"%1"
)) -- a2+d5f 2
print(string.gsub(s,
"()(m+)"
,
"%2"
)) -- am+dmf 2
print(string.gsub(s,
"()(m+)"
,
"%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,它是用匹配到的串去替换,简单来说就是重复匹配到的串,比如这样:
1
2
|
local s =
"am+dmf"
print(string.gsub(s,
"()(m+)"
,
"%0%0%0"
)) -- ammm+dmmmf 2
|
1
2
|
local s =
"am+dmf"
print(string.gsub(s,
"()(m+)"
,
"%%"
)) -- a%+d%f 2
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
local s =
"am+dmf"
local t1 = {
[2] =
"hh"
,
[5] =
"xx"
,
}
local t2 = {}
print(string.gsub(s,
"()(m+)"
, t1)) -- ahh+dxxf 2
print(string.gsub(s,
"()(m+)"
, t2)) -- am+dmf 2
local t3 = {
[2] =
false
}
print(string.gsub(s,
"()(m+)"
, t3)) -- am+dmf 2
local t4 = {
[2] = { 123 }
}
print(string.gsub(s,
"()(m+)"
, t4)) -- error : invalid replacement value ( a table )
|
1
2
3
4
5
6
7
8
9
10
|
local s =
"am+dmf"
function
f1(...)
print(...) -- 2 m -- 5 m
return
"hh"
end
function
f2()
return
{ 123 }
end
print(string.gsub(s,
"()(m+)"
, f1)) -- ahh+dhhf 2
print(string.gsub(s,
"()(m+)"
, f2)) -- error : invalid replacement value ( a table )
|
1
2
3
4
5
6
|
local s =
"am+dmf"
print(string.gsub(s,
"()(m+)"
,
"%%"
, -1)) -- am+dmf 0
print(string.gsub(s,
"()(m+)"
,
"%%"
, 0)) -- am+dmf 0
print(string.gsub(s,
"()(m+)"
,
"%%"
, 1)) -- a%+dmf 1
print(string.gsub(s,
"()(m+)"
,
"%%"
, 2)) -- a%+d%f 2
print(string.gsub(s,
"()(m+)"
,
"%%"
, 3)) -- a%+d%f 2
|
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
|
static
int
str_gsub (lua_State *L) {
size_t
srcl;
const
char
*src = luaL_checklstring(L, 1, &srcl);
const
char
*p = luaL_checkstring(L, 2);
int
max_s = luaL_optint(L, 4, srcl+1);
int
anchor = (*p ==
'^'
) ? (p++, 1) : 0;
int
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) {
const
char
*e;
ms.level = 0;
e = match(&ms, src, p);
if
(e) {
n++;
add_value(&ms, &b, src, e);
}
if
(e && e>src)
/* non empty match? */
src = e;
/* skip it */
else
if
(src < ms.src_end)
luaL_addchar(&b, *src++);
else
break
;
if
(anchor)
break
;
}
luaL_addlstring(&b, src, ms.src_end-src);
luaL_pushresult(&b);
lua_pushinteger(L, n);
/* number of substitutions */
return
2;
}
|
1
2
3
4
5
6
|
local a =
"ammmf"
print(string.match(a,
"%a"
)) -- a
print(string.match(a,
"%a*"
)) -- ammmf
print(string.match(a,
"%a+"
)) -- ammmf
print(string.match(a,
"%a-"
)) --
print(string.match(a,
"%a?"
)) -- a
|
1
2
3
|
local a =
"ammmf"
print(string.match(a,
"()c"
)) -- nil
print(string.match(a,
"()c?"
)) -- 1
|
1
2
|
local a =
"aaabb"
print(string.match(a,
"%bab"
)) -- aabb
|