本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.9节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.9 分组和捕获匹配中的子串
问题描述
改进匹配Mary、Jane或Sue的正则表达式,使之只能匹配完整单词。使用分组来实现这个功能,整个正则表达式只需要一对单词分界符,而不是给每个选择分支都使用一对分界符。
创建一个正则表达式,使之匹配yyyy-mm-dd格式的任意日期,并且分别捕获年、月和日。目标是在处理匹配的代码中可以更容易处理这些分别捕获的值。你可以假设目标文本中的所有日期都是合法的。正则表达式不必要考虑去掉像9999-99-99这样的非法数据,因为它们根本不可能出现在目标文本中。
解决方案
\b(Mary|Jane|Sue)\b
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
\b(\d\d\d\d)-(\d\d)-(\d\d)\b
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
在上一节中介绍过的选择分支操作符(alternation operator)在所有正则操作符中拥有最低的优先级。如果你尝试的是‹bMary|Jane|Sueb›,那么三个选择分支分别是‹bMary›、‹Jane›和‹Sueb›。这个正则式会匹配Her name is Janet中的Jane。
如果想要正则表达式中的一些内容不受选择分支影响的话,那么你就需要把这些选择分支进行分组。分组是通过圆括号来实现的。它们拥有在所有正则操作符中的最高优先级,这与绝大多数编程语言都是一致的。‹b(Mary|Jane|Sue)b›拥有三个选择分支‹Mary›、‹Jane›和‹Sue›—它们都位于两个单词分界符之间。这个正则式在Her name is Janet中不会匹配任何单词。
当正则引擎到达目标文本中的Janet的J位置时,会匹配第一个单词分界符,接着该引擎就会进入这个分组。分组中的第一个选择分支‹Mary›匹配失败。第二个选择分支‹Jane›匹配成功。然后引擎就会退出该分组。只剩下了‹b›。然而单词分界符无法匹配这里的e与目标结尾处的t之间的位置。在J位置开始的匹配其最后的结果是失败。
一组圆括号不仅仅是一个分组;它还是一个捕获分组(capturing group)。对于前面的Mary-Jane-Sue的正则表达式来说,捕获并不是很有用,因为它只是简单地匹配这个正则表达式。当需要包含部分正则表达式的时候,捕获才会有用,就像在正则式‹b(dddd)-(dd)-(dd)b›中。
这个正则表达式匹配一个yyyy-mm-dd格式的日期。正则表达式‹bdddd-dd-ddb›也会匹配完全一样的内容。因为这个正则式没有使用任何选择分支或者重复,因此圆括号的分组功能就没必要存在。不过捕获功能用起来是很方便的。
正则表达式‹b(dddd)-(dd)-(dd)b›拥有三个捕获分组。分组是按照左括号的顺序从左向右进行编号的。‹(dddd)›是1号分组,‹(dd)›是2号,第二个‹(dd)›是3号分组。
在匹配过程中,当正则表达式引擎到达右括号而退出分组的时候,它会把该捕获分组所匹配的文本的子串存储起来。当我们的正则式匹配2008-05-24的时候,2008被存储到第1个捕获分组中,05在第2个捕获分组,而24则在第3个捕获分组中。
有3种使用捕获文本的方式。实例2.10会讲解如何在同一个正则匹配中再次匹配所捕获的文本。实例2.21会展示在执行查找和替换的时候,如何把捕获到的文本添加到替代文本中。实例3.9会介绍在你的应用程序中如何使用正则匹配的子串。
变体
非捕获分组
在正则式‹b(Mary|Jane|Sue)b›中,我们使用圆括号只是为了分组的目的。与其使用一个捕获分组,我们也可以使用非捕获分组。
\b(?:Mary|Jane|Sue)\b
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
由三个字符‹(?:›作为起始的是一个非捕获分组。右括号‹)›则作为该分组的结束。非捕获分组提供相同的分组功能,但是不会捕获任何内容。
在计算捕获分组的左括号来确定其序号的时候,不会计算非捕获分组的括号。这也是非捕获分组的主要好处:你可以把它们添加到已有的正则表达式中,而不会破坏对于已经编号的捕获分组的引用。
非捕获分组的另外一个好处是它的性能较高。如果你不会使用到某个特定分组的反向引用(实例2.10),需要把它重新插入到替代文本中(实例2.21),或是在源代码中提取其匹配(实例3.9)的话,那么使用捕获分组就会添加额外的性能开销,这可以通过使用非捕获分组来消除。在实践中,你可能很少会注意到性能上的差异,除非在一个代码很少但循环次数很多的“紧闭循环”(tight loop)中使用正则表达式,并且/或者使用它来处理大量数据。
带模式修饰符的分组
在实例2.1的“不区分大小写的匹配”变体中,我们解释了.NET、Java、PCRE、Perl和Ruby支持局部模式修饰符,其中使用了模式切换:‹sensitive(?i)caseless (?-i)sensitive›。虽然这种语法也会涉及到圆括号,但像‹(?i)›这样的切换并不会影响到任何分组。
如果不用切换,你也可以在非捕获分组中来指定模式修饰符。
\b(?i:Mary|Jane|Sue)\b
正则选项:无
正则流派:.NET、Java、PCRE、Perl、Ruby
sensitive(?i:caseless)sensitive
正则选项:无
正则流派:.NET、Java、PCRE、Perl、Ruby
在非捕获分组中添加模式修饰符会对在该分组中的正则表达式子串设置该模式。而在右括号之后则会恢复之前的设置。因为默认来说正则表达式是区分大小写的,因此只有在 (?i:⋯) 之内的正则表达式部分才是不区分大小写的。