构成元素与方法 中
书接上回
使用上一节介绍的基本元素,你已经可以自己构建一些固定结构的表达式了。比如匹配任意一个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个:
-
限定量词的使用范围,如
\w\d{5}和(\w\d){5}匹配结果是不一样的,前者匹配1个字符和5个数字共6位,后者匹配5个(字符+数字组合)共10位; -
将可选分支|局部化,可选分支默认是整体的,如
(cat)(dog)|(ant)匹配catdog或ant,而使用了子组后(cat)(dog|ant)匹配的是catdog或catant,可选分支变为了子组里面的内容; -
单独捕获匹配结果,如果不用子组的话,正则返回值是整个匹配字符串,而用了子组后,在返回整个字符串同时,还能返回该子组的匹配结果,用来做数据提取非常方便!
-
后向引用,可以直接引用前面子组成功捕获的结果。
-
根据判断条件,选择可选分支,这是条件子组的应用。
-
代码注释,直接在正则表达式中进行注释,方法是(?@后面跟注释内容)
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点:
-
如果一个子组不能够得到匹配结果,此时任何对这个子组的后向引用也都会失败。
-
如果在表达式中没有足够多的捕获组,将会报错。
-
如果之前有一个子组是”非捕获子组“,则不计数排序,直接跳过。
-
后向引用的是前面子组的匹配结果,而不是特征,看下面两个例子:
(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})+,这是一个自引用的案例,我们拆开看一下,
-
第一轮匹配:这时只能匹配字符串a,因为\g{1}还没有赋值,因为它所引用的对象(a|b\g{1})还没有得到匹配,而且分支b\g{1}没有意义无法匹配;
-
第二轮匹配:这个时候\g{1}的值已经被设为a,现在这个表达式就可以翻译成
a(a|b(a)),可以匹配字符串aa或者aba,如果匹配aa,那么\g{1}之后每一轮的值都是a,这个表达式就可以匹配任意个a,但如果匹配aba,那么\g{1}的值就会被设置为ba,这是(a|b(a))第二轮的捕获值,然后我们再看第三轮; -
第三轮匹配:\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;
欧克,子组的相关内容有点儿多,我分两篇讲,先总结一下,我们了解了子组和它的作用,并且知道了什么叫捕获以及后向引用,以及使用时的注意事项,最后说到了条件子组,并简单介绍了递归引用。
我们再回头看一下文章最开始的那个问题,如何用正则匹配任意一个IP地址,应用咱们今天学的内容。
- 第一步,分析目标基本结构。IP地址的范围从0.0.0.0-255.255.255.255,学过了引用的知识,我们只需要把第一个字段解决了,后面的3个字段直接引用就好,不需要再重复。
- 第二步,设置限制范围。0-255需要限制范围,否则就会出现刚开头提到的问题,这里就要用到子组+可选分支的结构,我们先看200-255这个范围,可以写成
2(5[0-5]|4\d) - 接下来看0-199这个范围,我们如果简单写成
[01]?\d?\d,那么会造成299匹配成99,会把一个不合规的数据部分提取成合规的。我们要的效果是不合规的数据报错或完全不匹配。那么我们需要对目标位数和范围做更强的限定。 - 第三步位数和范围限定,我们先看3位数的情况100-199这个范围,可以用1\d\d来固定匹配。再看两位数的情况10-99这个范围,可以用[1-9]\d来固定匹配,最后一位数\d。这样还不够,我们还需要加上前后两个锚点:
^(1\d\d|[1-9]\d|\d)\.通过前后两个锚点和固定的位数限制,我们就限制死了对象的位数和范围,对于超出范围的值都不做任何匹配。 - 这里需要注意一个特殊情况,就是000,010,这类以0开头的3位数,我们通过0\d{2}来匹配。
- 第四步组合。我们设置了5个可选分支,先将最复杂的分支放在最前面
^(2(5[0-5]|4\d)|1\d{2}|0\d{2}|[1-9]\d|\d)\.后面3组就是对第一个子组的递归调用,形成最终结果:^^(2(5[0-5]|4\d)|1\d{2}|0\d{2}|[1-9]\d|\d)(\.(?1)){3}$ - 翻译一下,我这里用了递归引用,引用的是第一个子组的表达式。这里没有做后向引用,是因为后向引用的对象是子组捕获值,在这里引用捕获值是错的。另外将(.(?1)){3}放到一起然后重复3次,用到了这节最开始提到的子组与量词的搭配。
关于递归的详细知识,我会在后面详细讲,看到这里还没放弃的,下面的内容会更精彩!
学习不易,请勿私自转载,否则别怪老大爷不客气。