正则表达式之二——捕获组、非捕获组、环视、固化分组、反向引用、回溯

一、捕获组(capturing group)

捕获组分为普通捕获组(Expression)和命名捕获组(?<name>Expression)

捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。

捕获型括号的编号是按照括号出现的次序,从左到右计算的。如果提供反向引用,可以在表达式的后面用\1,\2来引用匹配的文本。如:(a)(b)可以用\1引用(a),\2引用(b).
捕获和非捕获:使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个捕获组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。

如图1所示


图1


也可以自己指定组名即命名捕获组。这样在表达式或程序中可以直接引用组名,当然也可以继续使用组号。但如果正则表达式中同时存在普通捕获组和命名捕获组,那么捕获组的编号就要特别注意,编号的规则是先对普通捕获组进行编号,再对命名捕获组进行编号。

如图2所示


图2


二、非捕获组(non-capturing group)

非捕获组(?:Expression)只匹配结果,但不捕获结果,也不会分配组号,非捕获组的价值体现在几个方面:
1. 能够使复杂的表达式清晰,不用担心用到$1会产生混乱。
2. 有助于提高效率。因为引擎不需要记录捕获到的匹配内容,从而减少内存使用加快匹配速度。

图-3 和 图-4 所显示的事例展示了捕获组与非捕获组为实现相同目的不同表现

为了获取某个已逗号为列分割的文件的中的某几列,将第一列与第九列合并,添加一列空列并将导数第四和第五列取出

图-3中使用的非捕获组以避免在引用编号时造成混淆。

图-3


图-4


三、反向引用

捕获组捕获到的内容,可以正则表达式正则表达式内部或外部应用程序进行引用,这种引用方式就是反向引用。

捕获组在匹配成功时,会将子表达式匹配到的内容,保存到内存中一个以数字编号或指定组名的组里,可以看做是一个赋值了的局部变量,这时就可以通过反向引用方式,引用这个局部变量的值。一个捕获组在匹配成功之前,它的内容可以是不确定的,一旦匹配成功,它的内容就确定了,反向引用的内容也就是确定的了。

(具体事例参考图-3和图-3)

注意:在使用普通捕获组时以数字编号被反向引用调用时,如果捕获组编号大于等于10时,则根据不同的语言环境会有不同的解析(大多数情况下回被看做第1个捕获组的反向引用加一个普通字符“0”,有些则会看做第10个捕获组的反向引用);在实际应用场景下,10个以上捕获组的反向引用几乎不存在,对它的研究通常仅存在于理。而对于太多的捕获组容易造成混淆,可以通过非捕获组或命名捕获组来解决。


四、环视(lookaround)

环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的因此有些地方也称之为零宽断言。环视匹配的最终结果就是一个位置。

环视按照方向划分有顺序lookabead和逆序(lookbehind)两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。


顺序肯定环视
(?=Expression),表示所在位置右侧能够匹配Expression
顺序否定环视
(?!Expression),表示所在位置右侧不能匹配Expression
逆序肯定环视
(?<=Expression),表示所在位置左侧能够匹配Expression
逆序否定环视
(?<!Expression),表示所在位置左侧不能匹配Expression

环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功;顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件。顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。

环视的综合应用实例:

将数字格式化成用“,”的货币格式

如图-5所示,


图-5

首先是逆序肯定环视表示左边必须是数字;其次是逆序否定环视表示左边不能有小数点符号;之后是顺序肯定环视表示该位置右边至少有一组由3位数字组成的字符窜,且数字最右边为小数点或者结束符。


五、固化分组(atomic groups)

使用固化分组的匹配与正常的匹配并无差别,但是如果匹配进行到此结构之后(也就是,进行到闭括号之后),那么此结构体中的所有备用状态都会被放弃。也就是说,在固化分组匹配结束时,它已经匹配的文本已经固化为一个单元,只能作为整体而保留或放弃。括号内的子表达式中未尝试过的备用状态都不复存在了,所以回溯永远也不能回到固化分组的位置。固化分组能够提高匹配效率,而且能自己控制什么能匹配和什么不能匹配。

以"(\.\d\d(?>[1-9]?))\d+"为例在固化分组内,量词能够正常工作,所以如果「[1-9]」不能匹配,正则表达式会返回「? 」留下的备用状态。然后匹配脱离固化分组,继续前进到「\d+」。在这种情况下,当控制权离开固化分组时,没有备用状态需要放弃(因为在固化分组中没有创建任何备用状态)。
如果「[1-9]」能够匹配,匹配脱离固化分组之后,「? 」保存的备用状态仍然存在。但是,因为它属于已经结束的固化分组,所以会被抛弃。匹配‘.625’或者‘.625000’时就会发生这种情况。在后一种情况下,放弃那些状态不会带来任何麻烦,因为「\d+」匹配的是‘.625000’,到这里正则表达式已经完成匹配。但是对于‘.625’来说,因为「\d+」无法匹配,正则引擎需要回溯,但回溯又无法进行,因为备用状态已经不存在了。既然没有能够回溯的备用状态,整体匹配也就失败,‘.625’不需要处理,而这正是我们期望的。


六、回溯(backtracking)
上面提到了回溯,下面就介绍一下回溯。

当正则表达式模式包含可选限定符或替换构造时,会发生回溯,并且正则表达式引擎会返回以前保存的状态,以继续搜索匹配项。 回溯是正则表达式的强大功能的中心;它使得表达式强大、灵活,可以匹配非常复杂的模式。 同时也付出性能等代价。 通常,回溯是影响正则表达式引擎性能的单个最重要的因素。 
当一个正则表达式扫描目标字符串时,从左到右逐个扫描正则表达式的组成部分,在每个位置上测试能不能找到一个匹配。对于每一个量词和分支,都必须确定如何继续进行。如果是一个量词(如*、+?或者{n,}),那么正则表达式必须确定何时尝试匹配更多的字符;如果遇到分支(如|操作符),那么正则表达式必须从这些选项中选择一个进行尝试。
当正则表达式做出这样的决定时,如果有必要,它会记住另一个选项,以备返回后使用。如果所选方案匹配成功,正则表达式将继续扫描正则表达式模板,如果其余部分匹配也成功了,那么匹配就结束了。但是,如果所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,然后在剩余的选项中选择一个。继续这样,直到找到一个匹配,或者量词和分支选项的所有可能的排列组合都尝试失败后放弃这一过程,然后移动到此过程开始位置的下一个字符上,重复此过程。
回溯就像在道路上遇到每个分岔口留下标记,如果走了死路可以照原路返回知道遇见标记尚未尝试过的到绿,如果那条路也不通,可以继续返回,找到下一个标记知道找到出路或尝试完全部的路。
回溯机制:当回溯进行时,决定先取那个状态匹配,如果需要在“进行尝试”和“跳过尝试”间选择,取决于量词(如:*、+、??等),如果是匹配优先量词(如:+、*)则先选择“进行尝试”,否择选则“跳过尝试”;如果需要了解回溯时使用的是哪一个(或者是哪些)之前保存的分支时,应当选择距离最近存储的选项(即使用后进先出的原则)。

事例:
表达式:^.*([0-9][0-9])
字符串:abcd1234abcd

由于表达式开头为优先匹配量词(*),因此首先获取整个字符串进行匹配;之后是两个数字,引擎开始回溯,匹配字符窜“abcd1234ab”,还是不匹配,继续回溯“匹配字符窜“abcd1234”匹配到结尾时两个数字,返回成功。


备用状态:
回溯中的那些标记就相当于备用状态,他们用来标识在需要的时候,引擎可以从这里开始重新尝试匹配。他们记录了正则表达式中的位置和未尝试的分支的字符串中的位置。



参考资料:《精通正则表达式》


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值