关于lua正则表达式的一些诡异表现和思考

        博主最近在学习 Lua,一边看项目的源码,有时看见一些复杂的正则表达式匹配模式,就想试试匹配出来的效果如何(如果我抱着想当然的心态:它肯定是这样匹配,不用多想了!就不会有这篇文章和这些思考了)。

        开始之前,先介绍一下 Lua 的 string.gsub 函数用法以及正则表达式的一些匹配规则(其中英文版文档来自Lua官方网站 http://www.lua.org/manual/5.1/manual.html  中文文档来自《Openresty最佳实践》一书):

关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

        英文文档会更全一点,但大意和中文文档是一样的。好了,有个大概认识了(我就不再赘述上面贴出的图片了,假设到这里你们都已阅读过前面的贴图),来看看以下代码的输出(让我觉得很诡异):

关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

        让我觉得诡异的地方是, .*  匹配 hello123world 这个字符串居然出现了 2 次匹配;%a* 匹配 hello123world 出现 6 次匹配而且一前一后各出现了 2 次(前后各出现一次的话能正常理解);最最最诡异的还是 .- 匹配,居然在每一个字符之间插入替换字符,而且整个字符串的一前一后还各有一个!

        再来看三组情况:

        1) 

 关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

        我想,在每个例子下面阐述我的理解会比较清晰,省得自己还有读者又往回看例子。我的理解可能有些牵强,但这是我目前唯一能想到的“合理”解释了,如果有更好的解释,欢迎提出!

对于第一个匹配(第7行) .* 匹配前面任意(按照我老师的说法是“所有”,详见前面贴图英文文档)字符且是 * ,匹配 0 个或多个,贪婪模式匹配(最大匹配),目标字符串是一个空字符串, 因此匹配 0 个(匹配了 0 个字符符合 ≥0 也算是匹配成功因为是使用 * ,因此用 # 代替,并且 string.gsub 第二个返回值 +1 ),故 string.gsub 返回值为 (#  1);

对于第二个匹配(第8行) .+ 匹配前面任意字符且是 + ,匹配 1 个或多个,贪婪模式匹配(最大匹配),目标字符串是一个空字符串,因此只匹配了 0 个(匹配了 0 个字符,不符合 ≥ 1,因此不算匹配成功,不会用 # 代替,string.gsub 第二个返回值为 0),故 string.gsub 返回值为 (原字符串即空字符串  0);

对于第三个匹配(第9行) %a* 匹配前面任意(按照我老师的说法是“所有”,详见前面贴图英文文档)字母且是 * ,匹配 0 个或多个,贪婪模式匹配(最大匹配),目标字符串是一个空字符串, 因此匹配 0 个(匹配了 0 个字母符合 ≥0 也算是匹配成功因为是使用 * ,因此用 # 代替,并且 string.gsub 第二个返回值 +1 ),故 string.gsub 返回值为 (#  1);

对于第四个匹配(第10行) %a+ 匹配前面任意字母且是 + ,匹配 1 个或多个,贪婪模式匹配(最大匹配),目标字符串是一个空字符串,因此只匹配了 0 个(匹配了 0 个字母,不符合 ≥ 1,因此不算匹配成功,不会用 # 代替,string.gsub 第二个返回值为 0),故 string.gsub 返回值为 (原字符串即空字符串  0);

对于第五个匹配(第11行) .- 匹配前面任意字符且是- ,匹配 0 个或多个,非贪婪模式匹配(最小匹配,其实可以理解为都匹配 0 个,经过了大量测试,个人感觉这个 - 是个坑,因为 0 个或多个且非贪婪匹配那就是每次都匹配 0 个呗,当然也有它的用武之地,当指定后缀的时候,详见结论),目标字符串是一个空字符串,因此只匹配了 0 个(匹配了 0 个字符,符合 ≥ 0 也算是匹配成功因为是使用 - ,因此用 # 代替,string.gsub 第二个返回值 +1 ),故 string.gsub 返回值为 (#  1);

        2)

关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

目标字符串为 "a"

对于第一个匹配(第7行),先是贪婪模式最大匹配任意字符,成功匹配整个字符串 "a"(匹配 1 个字符,算匹配成功,用 # 代替被匹配的内容 "a"string.gsub 第二个返回值 + 1),接着是重点了,我老师说(我个人也是这么认为吧)字符串的结尾(不知道lua是以什么标记字符串结尾的,是不是也像 C/C++ 那样使用 '\0')还会再匹配一次,这可能跟 string.gsub 实现有关,但我没看过 string.gsub 源码,也可能是 lua 正则表达式的一个坑(我老师认为 lua 正则表达式不同于 Perl 正则表达式且特别恶心)。那么当再次匹配字符串结尾的时候,匹配 0 个(匹配了 0 个字符,符合 ≥ 0 也算是匹配成功因为是使用 *因此又放一个 # 代替,string.gsub 第二个返回值又 + 1),故 string.gsub 返回值为 (##  2)

对于第二个匹配(第8行),先是贪婪模式最大匹配任意字符,成功匹配整个字符串 "a"(匹配 1 个字符,符合 ≥ 1,匹配成功,用 # 代替被匹配的内容 "a"string.gsub 第二个返回值 + 1),当再次匹配字符串结尾的时候,匹配 0 个(匹配了 0 个字符,不符合 ≥ 1 因此匹配不成功因为是使用 + ),故 string.gsub 返回值为 (#  1)

对于第三个匹配(第9行),情况同第一个匹配(第7行),只是字符和字母的区别

对于第四个匹配(第10行),先是贪婪模式最大匹配任意字母,成功匹配整个字符串 "a"(匹配 1 个字母,符合 ≥ 1,匹配成功,用 # 代替被匹配的内容 "a"string.gsub 第二个返回值 + 1),当再次匹配字符串结尾的时候,匹配 0 个(匹配了 0 个字母,不符合 ≥ 1 因此匹配不成功因为是使用 + ),故 string.gsub 返回值为 (#  1)

对于第五个匹配(第11行),先是非贪婪最小匹配任意字符,成功匹配 0 个(匹配 0 个字符,符合 ≥ 0,匹配成功,用 # 代替被匹配的内容——空字符串因为匹配 0 个字符并没有匹配中 a,因此 a 没被匹配还得往返回的字符串里写,现在返回字符串是 "#a",string.gsub 第二个返回值 + 1),当再次匹配字符串结尾的时候,匹配 0 个(匹配 0 个字符,符合 ≥ 0,匹配成功因为是用 - 匹配的,用 # 代替,string.gsub 第二个返回值 + 1),故 string.gsub 返回值为 (#a#  2)

        3)

关于lua正则表达式的一些诡异表现和思考关于lua正则表达式的一些诡异表现和思考

打字累啊,这个例子留待读者分析验证,按照上述的思路和理解是能说得通的。注意匹配是按照原字符串一位一位进行,匹配成功的采取替换(或者其他操作),不成功的往返回字符串里写(这是博主的假设)。


        博主用 Lua 的 string.gmatch 函数(不清楚的可以百度一下)做了同样的实验,也是得到以上“诡异”的结果。

        以上分析建立在两个假设的基础上:1. 对字符串结尾还做一次检查;2. 匹配是按照原字符串一位一位进行,匹配成功的采取替换(或者其他操作),不成功的往返回字符串里写。我让别人用 Python 做了同样的实验,结果是正常的能理解的(如利用正则表达式匹配模式 " .* " 匹配字符串 "hello123world",利用 # 代替被匹配内容,得到的结果是  (#  1) —— 匹配 1 次)。这有可能是 Lua 的 string 库函数实现的问题,也可能是 Lua 的正则表达式有异于别的,这个待有机会看到 Lua 的 string 库函数实现才能说个明白了。

        另外得出两个结论:

        1. - 很懒,能匹配 0 个它肯定匹配 0 个,当然在正则表达式匹配模式中指定后缀时能派上用场,举个例子:需要匹配 "2017/08/14/date/0000/date" 中的日期,可以像如下那样写:

关于lua正则表达式的一些诡异表现和思考

        2. 在 Lua 中直接像上述几个例子那样使用正则表达式(尤其是 * 匹配)很危险,结果可能超出预期,就算部分测试用例能通过,未来也可能给你带来意想不到的坑,应该就实际情况适当加上前缀后缀用以限制,尽可能规避未知风险。

  

        写这篇文章主要是给遇到同样问题的朋友一个参考,同时警醒自己。以上分析仅是本人愚见,暂时也得不到权威的证实。如果读者有更好、更正确的理解和分析,欢迎提出指正,以供共同学习共同进步。如果有人找到 Lua string 库函数源码,也欢迎分享。