已经拿Lua用了快两年的时间了,但是每次用到字符串的模式匹配的时候就总要去翻看Lua官网的说明,网上也没有一个比较详细的说明,也有好多朋友都向我询问这块的内容,其实这块的难点有三:


  • 一个是对Lua的正则表达式不熟悉;
  • 另一个是对Lua中string库提供的几个函数的用法不熟悉;
  • 还有一点是Lua的string库提出了一个新的概念,叫做捕获,其实也不算什么新概念,只是和函数调用杂糅在一起行为就不好理解罢了。


这里我总结一下。

先从Lua内置string库提供的几个大家不熟悉的函数开始(基于Lua5.1,Lua5.2基本没有变化)。


Lua内置字符串库用到模式的地方有4个函数,它们分别是:


  • string.find()
  • string.match()
  • string.gmatch()
  • string.gsub()

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正则表达式的介绍应该就能明白。


比如:


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


而当第四个参数为 true 的时候, + 被当作正常字符,那么查找的匹配就是 m+ 这个字符串,那么找到的位置就是 2     3。


如果你不传第四个参数,就跟 false 是一个意思。


上面把 find 函数做了一个简单的介绍,但是这个函数的行为并非总是这样,为什么呢?这就是我文章开头提到的Lua的捕获也会被杂糅到这些string的库函数里。


没有办法,只得先介绍一下所谓的捕获是个什么概念。


上面 find 函数的第二个参数我们都明白是一个模式,可以理解为一般的正则匹配中的正则表达式,而Lua为这个模式增加了一个新的功能,也就是所谓的捕获,在一个模式串中,我们可以用小括号()来标明一些我们想要保存的匹配,而这个小括号中的内容依然是模式串,也就是说我们只不过是把模式中一些我们想要的特殊字符保留下来供后面使用。比如上面那个例子中的模式串是 m+ ,如果我想要把跟m+

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



一个空的捕获,也就是小括号里面什么内容也没有,它会返回当前字符串的比较操作进行到的位置,比如



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的源码来调整你想要保存的捕获数量,这个数量定义在 luaconf.h 文件中:



lua判断是否为table lua判断不在列表内_lua



一般来说,对于使用,分析基本到此了,但是对于 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

lua判断是否为table lua判断不在列表内_字符串_02


如果没有这些字符或者是不对这些字符特殊处理,那么就是一个简单的字符串匹配,调用 lmemfind()函数,如果找到了,就返回了匹配到的起止位置。


既然如此,那么 else 里就好理解了,它就是使用特殊字符进行匹配的处理,这里的关键函数是match(),它处理字符串和模式进行匹配,并进行了捕获,这个留到介绍模式的时候再接着说。最后如果匹配到了,那么仍然返回匹配起止点,注意,这里多了一个操作,就是把捕获到的字符串也压入了栈。所以我们调用并捕获的时候才会有后面那些捕获的字符串。


这么看来还是挺好理解的嘛。在好奇心的趋势下,我非常感兴趣,Lua的那个 lmemfind() http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.htmlhttp://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html


怀着一点小激动,我点开了 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 函数的做法,这里有一篇文章,虽然是说他的优化,但是看他的代码也能大致了解 memcmphttp://blog.chinaunix.net/uid-25627207-id-3556923.html


2、string.match(s, pattern, 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)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,           "%a+"          )           do         


                    print(w)         


          end         


          --[[         


          hello         


          world         


          from         


          Lua         


          ]]



至于 %a+ 的意义嘛,在 string.find() 的介绍里提到过字符 + 的用法,至于 %a


string.gmatch() 函数的用法基本也就是在循环里当作迭代器用了,我还真没发现有别的用法。


一个唯一需要注意的地方,就是特殊字符 ^ 在 string.gmatch 函数中的用法跟别处是不同的,在其他的函数中,^


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 分支模式匹配里可以看到,有专门处理 ^



那么在 string.gmatch 函数中,如果把 ^


老样子,虽然可以想象得到 string.gmatch() 的实现应该跟上面的差不多,但还是看一眼源码比较保险:


前面那两行是返回Lua迭代器所需求的状态和迭代函数,不用去管它。让我们来看一下 gmatch_aux (lua_State *L)



4、string.gsub(s, pattern, rep, n)


这个函数跟 string.gmatch() 一样,也带一个 g,可以想象得到,这个函数也会获得所有的匹配字符串,而不像 string.find() 和 string.match() 一样,碰到一个就结束了。确实是这样。这个函数的作用是在字符串 s 中查找与模式 pattern 相匹配的所有串,然后用 rep 参数产生的字符串进行替换,你可能要问,为什么是 rep 产生的串,而不是 rep 自己呢?因为这里的 rep 除了可以是一个字符串外,还可以是一个函数,甚至可以是一个table。


当 rep 是一个字符串的时候,一般来说是当作一个普通的字符串来处理,也就是直接用这个字符串来替换匹配到的串,但是这里会特殊的处理符号 %,%


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,它是用匹配到的串去替换,简单来说就是重复匹配到的串,比如这样:


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

任意一个单字符表达的集合,包括 % 加单字符表达的集合后面都可以跟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个有的时候就是有没有匹配成功的关键,比如加上 ?



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个使用了模式的函数的具体用法。