正则表达式的秘密(三)构成元素与方法 中

书接上回

使用上一节介绍的基本元素,你已经可以自己构建一些固定结构的表达式了。比如匹配任意一个IP地址12.1.242.1, 我们可以使用元字符+量词的方式匹配,比如^[0-2]?\d?\d\.[0-2]?\d?\d\.[0-2]?\d?\d\.[0-2]?\d?\d$

但这么写的话299.299.299.299也能匹配了,显然超出了IP地址的范围,而且代码看起来不是那么简洁对吧,里面有很多重复项。那如何让正则更灵活,更简洁,更有效呢? 我们带着这个问题往下看吧。

子组 ()

也就是我们常见的小括号(),首先它可以进行嵌套,但这并不影响从左到右的匹配顺序,这和运算中的括号是两码事。

它的功能很强大,我总结了一下,主要有6个:

  1. 限定量词的使用范围,如\w\d{5}(\w\d){5}匹配结果是不一样的,前者匹配1个字符和5个数字共6位,后者匹配5个(字符+数字组合)共10位;

  2. 将可选分支|局部化,可选分支默认是整体的,如(cat)(dog)|(ant)匹配catdog或ant,而使用了子组后(cat)(dog|ant)匹配的是catdog或catant,可选分支变为了子组里面的内容;

  3. 单独捕获匹配结果,如果不用子组的话,正则返回值是整个匹配字符串,而用了子组后,在返回整个字符串同时,还能返回该子组的匹配结果,用来做数据提取非常方便!

  4. 后向引用,可以直接引用前面子组成功捕获的结果。

  5. 根据判断条件,选择可选分支,这是条件子组的应用。

  6. 代码注释,直接在正则表达式中进行注释,方法是(?@后面跟注释内容)

1和6都没什么可说的,我重点说一下2,3,4,5.

可选分支|局部化

这里讲一个我曾经犯错的例子:(a|b)*

我刚开始学正则,会把它理解成(a*|b*),后来发现如果这么拆分的话,会有问题,(a*|b*)可以匹配任意个aaaa或者任意个bbbbb,但不能匹配a与b的组合aabb/abababab;

正确的翻译是,(a|b)是要当作一个整体去看,那么(a|b)*实际等价的是’(a|b)(a|b)(a|b)….’它可以匹配任意ab的组合,理解这个对我们之后讲递归正则很有用。

捕获子组与引用 \g{n}

捕获Catch的意思就是匹配成功。捕获的目的是为了引用。引用的目的是为了结构简单,避免重复。

子组默认都是捕获子组,匹配成功都会返回子组的匹配值(捕获值)。子组排序是按照从左到右的顺序,从1开始排。

但当一个子组是重复的时,捕获到的该子组的结果是最后一次迭代捕获的值。

这一点理解不到位就会犯错。

举个例子:表达式'\w*(\d{3})'匹配abc123456,在返回整个字符串的同时,还会返回(\d{3})子组的匹配值456; 但\w*(\d){3}的返回值,除了整个字符串外,捕获子组的返回值是6,理解了这一点,在使用的时候就一定要注意子组的范围;

ok,捕获到了之后该引用了。引用的意思就是将捕获值再调用。在正则中,引用都是后向的,也就是说被引用的内容必须出现在引用符号的前面。

通过方法\n或\gn或\g{n}进行引用,n代表子组的序号,我推荐使用\g{n},因为结构更清晰

n可以是正数,0或负数。如果n为正数,表示从表达式开始正着数第n个子组;如果n为负数,就是从\g{n}前面倒着数第n个子组。举例:(foo)(bar)\g{-1} 可以匹配字符串 ”foobarbar”, (foo)(bar)\g{-2} 可以匹配 ”foobarfoo”.

如果n=0,\g{0}的含义就是引用表达式自身,这个我们在递归正则的时候具体讲。

引用时注意4点

  1. 如果一个子组不能够得到匹配结果,此时任何对这个子组的后向引用也都会失败。

  2. 如果在表达式中没有足够多的捕获组,将会报错。

  3. 如果之前有一个子组是”非捕获子组“,则不计数排序,直接跳过。

  4. 后向引用的是前面子组的匹配结果,而不是特征,看下面两个例子:

     (sens|respons)e and \g{1}ibility 将会匹配 ”sense and sensibility” 和 ”response and responsibility”, 而不会匹配 ”sense and responsibility”。
     ((?i)rah)\s+\g{1} 匹配 ”rah rah”和”RAH RAH”,但是不会匹配 ”RAH rah”或rah RAH
    复制代码

我再举一个比较特殊的引用例子:针对可选分支的后向引用。正则可以把引用直接嵌套在可选分支中使用,以达到匹配递归数列的作用,很有意思!

表达式(a|b\g{1})+,这是一个自引用的案例,我们拆开看一下,

  1. 第一轮匹配:这时只能匹配字符串a,因为\g{1}还没有赋值,因为它所引用的对象(a|b\g{1})还没有得到匹配,而且分支b\g{1}没有意义无法匹配;

  2. 第二轮匹配:这个时候\g{1}的值已经被设为a,现在这个表达式就可以翻译成a(a|b(a)),可以匹配字符串aa或者aba,如果匹配aa,那么\g{1}之后每一轮的值都是a,这个表达式就可以匹配任意个a,但如果匹配aba,那么\g{1}的值就会被设置为ba,这是(a|b(a))第二轮的捕获值,然后我们再看第三轮;

  3. 第三轮匹配:\g{1}的值这个时候被设置为ba,现在这个表达式就可以翻译成aba(a|b(ba)),可以匹配abaa或ababba,说到这里应该就可以看出规律了,这个表达式可以匹配ababbabbbab…a的递归数列。

条件子组

条件子组给了我们更大的匹配灵活度,它会根据条件判断而去匹配不同的特征。

通过方法(?(condition)yes pattern|no pattern)来实现,如果condition满足,则执行yes pattern,如果不满足则执行no pattern,no pattern也可以为空,就不会执行任何匹配。如果有超过2个的可选子组,会报错。

上面的内容不难理解,下面是重点,这里的condition支持3种使用方法

方法 模式 含义
(?(n)pattern) 数字型引用 将之前子组的捕获值,作为判断条件。子组匹配成功则为true,n是整数,可以为正,也可以为负,含义和后向引用一样,不再重复。
(?(R)pattern) 递归式引用 这里的R,指代的是整个表达式,如果表达式被递归调用的话,就会为true,但是在第一轮匹配时,条件总是false,因为第一轮的时候还没有发生递归调用;
?(断言)pattern) 将断言作为条件 这里的断言可以任意形式,前瞻,后顾,正,负都可以,断言的内容,我们下一节具体讲。

举例 数字型condition: (\))?[^()]+(?(1)\))这个表达式匹配一个没有括号的或者闭合括号包裹的字符串。拆分如下:

(\))? //配一个左括号,并且设置其为捕获值

[^()]+ //匹配一个或多个非括号字符

(?(1)\)) //是一个条件子组,它会测试第1个子组是否匹配,如果匹配到了,也就是说目标字符串以左括号开始,条件为true,那么使用 yes-pattern也就是这里需要匹配一个右括号)。其他情况下,既然 no-pattern 没有出现,这个子组就不匹配任何东西。

举例 递归式condition: A(?(R)B)(?R)?C 匹配AC或AABCC,我翻译一下:

第一轮匹配,首先匹配字符A;条件子组(?(R)B)不会匹配任何值,因为还没有进行递归;(?R)?表示递归0次或一次,这里我们先进行一个占位;最后匹配一个字符C,所以第一轮的匹配结果是A_C;

第二轮匹配,整个表达式进行递归,首先再次匹配字符A;然后(?(R)B)判断为true,匹配B;这里我们已经递归了一次,所以跳过(?R),直接匹配C,将第二轮匹配的ABC插入到之前的占位符里,得到的结果就是AABCC;

欧克,子组的相关内容有点儿多,我分两篇讲,先总结一下,我们了解了子组和它的作用,并且知道了什么叫捕获以及后向引用,以及使用时的注意事项,最后说到了条件子组,并简单介绍了递归引用。

总结不易,请勿私自转载。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享