已经拿Lua用了快两年的时间了,但是每次用到字符串的模式匹配的时候就总要去翻看Lua官网的说明,网上也没有一个比较详细的说明,也有好多朋友都向我询问这块的内容,其实这块的难点有三:
这里我总结一下。
Lua内置字符串库用到模式的地方有4个函数,它们分别是:
string.find()
string.match()
string.gmatch()
string.gsub()
1、string.find(s, pattern, start, plain)
这个函数的功能是查找字符串 s 中的指定的模式 pattern。
local s = "am+df" print(string.find(s, "m+", 1, false)) -- 2 2 print(string.find(s, "m+", 1, true)) -- 2 3
其中字符 + 在 Lua 正则表达式中的意思是匹配在它之前的那个字符一次或者多次,也就是说 m+ 在正则表达式里会去匹配 m, mm, mmm ……。所以当 string.find 第四个参数为 false 的时候,就只能在字符串 s 中找到 m 这个字母是匹配的,那么返回的结果就是 2 2。
local s = "am+df" print(string.find(s, "(m+)", 1, false)) -- 2 2 m
如果你想要捕获更多的内容,只需要用小括号把它括起来就好了,比如这样:
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
关于捕获还有一点需要说明的,就是捕获只会在模式能够匹配成功的时候才会跟着 string 的函数进行返回,比如下面这个,我想捕获字母 a ,但事实上这个模式根本无法匹配到,所以肯定是无法返回的:
local s = "am+df" print(string.find(s, "(m+)(a)", 1, false)) -- nil
另外捕获返回的顺序,是依照左小括号的位置来定的,比如上面那个捕获了3个 m 的例子,第一个 m 其实是最外层的小括号捕获到的。为什么要提到捕获的顺序呢?因为我们可以使用 %n 来取得第n个捕获的字符串,至于取得对应的捕获有什么用处呢?这个在后面会介绍到。
local s = ”am+df“ print(string.find(s, "()(m+)()", 1, false)) -- 2 2 2 m 3
有一点也必须要提一下,就是在Lua5.1的源码当中,捕获字符串的数量是有限制的,默认是32个,也就是说你添加的小括号不能无限加,最多加32个。如果捕获超过限制,当然会报错了,比如:
local s = ”am+df“ print(string.find(s, "()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()", 1, false)) -- 捕获33个
一般来说,对于使用,分析基本到此了,但是对于 Lua,因为源码简单,而且优美,又是拿C语言写的,心痒难耐,必须要了解一下源码才解恨。
Lua内置库的加载方式就不说了,在各个大神的文章里都可以看到,我们直接来看 string.find() 这个函数,函数在 lstrlib.c 文件里:
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; }
这个函数初步看起来还是比较长的,但是仔细分析一下就发现其实是很简单的。前面那 6 行,就是接收前 3 个参数罢了,只不过处理了一下那个查找起始点参数,防止了超出字符串长度。最关键的地方就是紧接着的 if else 逻辑,find 是传进来的参数,对于 string.find 来说就是1,所以不用管它,认为它一直是真就 OK 了,既然提到这里了,那么是不是还有别的地方也会调用这个函数原型的,bingo!我们搜索一下就会发现,其实 string.match() 函数其实也是调用这个函数原型的,而它的 find 参数就是传递的 0 。哈哈,难道 string.match 函数其实跟 string.find 函数是一样的?
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); }
这个留到介绍 string.match 函数的时候再说。拉回来,继续谈这个 if else 逻辑,if 的判断条件其实就是看你调用 string.find 的第四个参数,如果第四个参数传递了 true,也就是我上面说的,不使用特殊字符模式,或者是模式中压根就没有特殊字符,那个 SPECIALS 宏同样定义在这个文件中:
怀着一点小激动,我点开了 lmemfind() :
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 */ } }
总的来说,这个比较的方法还是中规中矩的,从头开始查找匹配串的第一个字符,只不过用的是 memchr 函数,找到了之后用 memcmp 函数来比较两个字符串是否是相同的,如果不相同就跳过检查了的字符继续。相比那些复杂的字符串匹配算法,这个既简单又可爱,赞一个:),memcmp 函数的执行自然比 str 系列的比较要快一些,因为不用一直检查 ‘\0’ 字符,关于 memcmp 函数的做法,这里有一篇文章,虽然是说他的优化,但是看他的代码也能大致了解 memcmp 的做法:http://blog.chinaunix.net/uid-25627207-id-3556923.html
在介绍 string.find 函数的时候提到过,Lua 源码中 string.match 调用的函数其实跟 string.find 调用的函数是相同的,都是 str_find_aux(lua_State *L, int find) 函数,唯一不同的地方在于 string.match 调用时 find 参数传递的是 0,这样就不会进入 str_find_aux() 里简单匹配的分支,直接进行模式匹配。
s = "hello world from Lua" for w in string.gmatch(s, "%a+") do print(w) end --[[ hello world from Lua ]]
至于 %a+ 的意义嘛,在 string.find() 的介绍里提到过字符 + 的用法,至于 %a 嘛,它是匹配所有的字母。这里需要注意的是,字符串 s 里由 4 个单词,用 3 个空格进行了分隔,所以调用一次 string.gmatch(s, "%a+"),只会匹配 s 中的第一个单词,因为遇到空格匹配就失败了。
local s = "am+df" print(string.find(s, "(m+)", 1, false)) -- 2 2 m print(string.find(s, "^(m+)", 1, false)) -- nil
第二个匹配,因为在模式前面增加了 ^ ,所以会从字符串 s 的最开始就进行匹配,也就是从字母 a 开始匹配,a 当然无法和 (m+) 匹配成功了,所以直接就返回 nil 了。这个处理在上面讲 string.find 的时候源码函数 str_find_aux() 里 else 分支模式匹配里可以看到,有专门处理 ^ 字符的代码。
前面那两行是返回Lua迭代器所需求的状态和迭代函数,不用去管它。让我们来看一下 gmatch_aux (lua_State *L) 函数,刨去为了迭代器做处理之后,就和 string.match() 函数实现没有什么区别了,最后都调用 match() 函数进行模式匹配。不同的地方就是上面说的字符 ^ 的处理这里是没有的。
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
local s = "am+dmf" print(string.gsub(s, "()(m+)", "%0%0%0")) -- ammm+dmmmf 2
匹配到的串是 m,用 mmm 替换了原串中的 m。
你可能要问,既然 % 被单独处理了,那么我想要用 % 去替换怎么办,只需要用 %% 就可以表示 % 自身了。比如:
local s = "am+dmf" print(string.gsub(s, "()(m+)", "%%")) -- a%+d%f 2
当rep是一个table的时候,每次匹配到了之后,都会用第一个捕获作为key去查询这个table,然后用table的内容来替换匹配串,如果没有指定捕获,那么,就用整个匹配串作为key去查询,如果没有查到对应key的值,或者对应的值不是字符串和数字,那么就不做替换:
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 )
当rep是一个函数的时候,每当匹配到字符串的时候,就把模式所有的捕获按照捕获顺序作为参数传递给这个函数,如果没有指定捕获,则传递整个匹配的字符串给函数,函数的返回值如果是字符串或者是数字就替换掉匹配,如果不是则不做替换:
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 )
第四个参数,用来表明,需要替换到第几个匹配为止,比如:
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
依然来看看源码是怎么写的:
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; }
可以看到它处理了符号 ^ ,循环进行匹配,如果匹配到了,就按照不同的类型把替换串添加进结果里,最后把所有字符压回栈上。
总的来说 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, "%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
看了上面的例子,你可能会想,那 * 和 + 或者加不加 ? 有什么区别呢?是有区别的,因为匹配0个和匹配1个有的时候就是有没有匹配成功的关键,比如加上 ? 就可以匹配0个,意味着即使没有对应集合的内容,也算匹配成功了,如果有捕获的话,这个时候捕获是生效的。比如:
local a = "ammmf" print(string.match(a, "()c")) -- nil print(string.match(a, "()c?")) -- 1
如果你现在还不知道 string.match() 是什么意思,就翻到前面去看吧。
还有一个特殊的字符需要介绍,就是 %b 后面跟两个不同的字符xy,它的意思是匹配从x开始,到y结束的字符串,而且要求这个字符串里x和y的数量要相同。比如 %b() 就是匹配正常的小括号,如下:
local a = "aaabb" print(string.match(a, "%bab")) -- aabb
最后,我在介绍 string.gmatch 的时候介绍过字符 ^ 的用法,它放在模式的首部,意思是从原串的首部就开始匹配,这里还有一个特殊字符跟它的用法类似,它就是 $ 字符,这个字符放在模式的末尾,意思是从原串的尾部开始匹配。在其他位置就跟 ^ 一样,也没有意义。
捕获
捕获的意思在介绍 string.find 的时候已经详细介绍过了,这里再提一笔,捕获是在模式中,用小括号括起来的子模式,它在匹配发生的时候截取小括号内模式匹配到的字符串,然后保存下来,默认最多保存 32 个,可以在Lua源码中修改保存的数量。另外捕获的顺序是按照小括号左括号的位置来定的。至于捕获如何使用,请参看我上面介绍的4个使用了模式的函数的具体用法。
本文出自 “菜鸟浮出水” 博客,请务必保留此出处http://rangercyh.blog.51cto.com/1444712/1393067
原文:http://rangercyh.blog.51cto.com/1444712/1393067