本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.11节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.11 捕获和命名匹配子串
问题描述
创建一个正则表达式,匹配yyyy-mm-dd格式的任意日期,并且分别捕获年、月和日。这样做的目的是为了在处理匹配的代码中可以更容易处理这些分别捕获的值。为了更好地实现这个目标,向每个捕获的文本添加描述性的名称:“year”、“month”和“day”。
再创建一个正则表达式来匹配yyyy-mm-dd格式的“神奇”日期。一个神奇日期指的是年份后2位与月份和日期都是相同的数字。例如,2008-08-08就是一个神奇日期。把神奇数字捕获下来(在这个例子中是08),并给它打上标签“magic”。
你可以假设目标文本中的所有日期都是合法的。正则表达式不必考虑去掉像9999-99-99这样的非法数据,因为它们根本不可能出现在目标文本中。
解决方案
命名捕获
\b(?<year>\d\d\d\d)-(?<month>\d\d)-(?<day>\d\d)\b
正则选项:无
正则流派:.NET、Java 7、XRegExp、PCRE 7、Perl 5.10、Ruby 1.9
\b(?'year'\d\d\d\d)-(?'month'\d\d)-(?'day'\d\d)\b
正则选项:无
正则流派:.NET、PCRE 7、Perl 5.10、Ruby 1.9
\b(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d)\b
正则选项:无
正则流派:PCRE 4 或更新版本、Perl 5.10、Python
命名反向引用
\b\d\d(?<magic>\d\d)-\k<magic>-\k<magic>\b
正则选项:无
正则流派:.NET、Java 7、XRegExp、PCRE 7、Perl 5.10、Ruby 1.9
\b\d\d(?'magic'\d\d)-\k'magic'-\k'magic'\b
正则选项:无
正则流派:.NET、PCRE 7、Perl 5.10、Ruby 1.9
\b\d\d(?P<magic>\d\d)-(?P=magic)-(?P=magic)\b
正则选项:无
正则流派:PCRE 4 或更新版本、Perl 5.10、Python
讨论
命名捕获
实例2.9和实例2.10讲解了捕获分组和反向引用。更加准确地来讲:这两个实例中使用的是编号捕获分组和编号反向引用。每个分组会自动获得一个编号,你可以使用这些编号来进行反向引用。
除了编号分组之外,现代的正则表达式流派还支持命名捕获分组。在命名和编号分组之间的唯一区别是你可以给分组指派一个描述性的名称,而不是被限制为只能使用自动分配的编号。命名分组可以使正则表达式更加容易阅读,更加容易维护。把一个捕获分组添加到已有的正则表达式中可能会修改之前指派给所有捕获分组的编号。但之前指派的名称不会变化。
Python是第一个支持命名捕获的正则表达式流派。它使用的语法是‹(?Pregex)›。名称中包含的必须是可以被‹w›匹配的单词字符。‹(?P›是分组的起始括号,而‹)›则是结束括号。
.NET Regex类的设计人员为命名捕获使用了自己的语法,他们使用了两种可以互换的变体。‹(?regex)›模仿了Python的语法,但是却去掉了其中的P。这里的名称必须由可以被‹w›匹配的单词字符组成。‹(?›是分组的起始括号,而‹)›则是结束括号。
如果你使用XML编码,或是像我们这样在DocBook XML中撰写本书,在命名捕获语法中的尖括号会给人带来麻烦。所以.NET中提供了另一种替代命名捕获语法:‹(?'name'regex)›。这里的尖括号被替换为了单引号。你可以出于自己的方便来选择使用哪种语法。它们的功能是完全等价的。
可能是基于.NET的流行超过了Python的原因,.NET语法似乎成为了其他正则库开发人员更乐于接受的语法。Perl 5.10及更新版本使用的是它,而在Ruby 1.9的Oniguruma引擎中也是如此。Perl 5.10和Ruby 1.9同时支持使用尖括号和单引号的语法。Java 7同样沿用了.NET的语法,但是仅支持尖括号这一方式。标准JavaScript不支持命名捕获。XRegExp通过使用.NET的语法为其添加了命名捕获支持,不过仅支持尖括号形式。
PCRE在很早的时候就沿用了的Python的语法,在那个时候Perl还根本没有提供对于命名捕获的支持。PCRE 7的版本中添加了Perl 5.10中添加的新功能,它同时提供对.NET语法和Python语法的支持。可能是基于PCRE成功的考虑,Perl 5.10采取了反向兼容的措施,也提供了对Python语法的支持。在PCRE和Perl 5.10中,用于命名捕获的.NET语法和Python语法的功能是完全一样的。
读者应当选择对你来说最为有用的语法。如果使用PHP编程,你会希望代码能够用于老版本的PHP,由于它采用的是PCRE旧版,你应该使用Python语法。如果不需要与旧版的兼容性,而且也同时使用.NET和Ruby的话,那么在这些语言之间进行复制和粘贴时,采用.NET语法更为容易。如果你不是很确定,那么可以使用PHP/PCRE中的Python语法。如果有人把你的代码在旧版PCRE下进行了重新编译,而代码中的正则表达式突然就不工作了,那么他们肯定会很不高兴。当你把一个正则表达式从.NET复制到Ruby中时,删掉一些P应该不是太困难。
PCRE 7和Perl 5.10的文档中几乎没有提到Python语法,但是并不意味着它要被淘汰。正相反,我们实际上推荐在PCRE和PHP中使用Python语法。
命名反向引用
有了命名捕获之后,紧接着就有了命名反向引用。正如命名捕获分组与编号捕获分组的功能完全相同一样,命名反向引用与编号反向引用的功能也是完全相同的。它们只是更加易于阅读和维护。
Python使用语法‹(?P=name)›来创建对分组name的反向引用。虽然该语法使用的是圆括号,但反向引用并不是一个分组。你不能在名称和结束括号之间放任何东西。一个反向引用‹(?P=name)›是一个独立的正则表达式记号,就像是‹1›一样。PCRE和Perl 5.10也支持Python的命名反向引用语法。
.NET使用的语法是‹k›和‹k'name'›。这两种形式在功能上是完全相同的,因此也可以随意混合使用。使用尖括号语法创建的命名分组可以采用引号语法来进行引用,反之亦然。Perl 5.10、PCRE 7和Ruby 1.9也支持.NET的命名反向引用语法。Java 7和XRegExp则仅支持尖括号形式。
我们强烈推荐你不要在同一个正则表达式中混合使用命名和编号分组。不同流派对于出现在命名分组之间的非命名分组的编号方法会遵循不同的规则。Perl 5.10、Ruby1.9、Java 7和XRegExp沿用了.NET的语法,但是它们并没有遵循.NET对于命名捕获分组,或者是混合使用编号捕获分组与命名分组进行编号的方式。与其在这里解释其中的差别,我们选择推荐大家不要把命名和编号分组混合使用。应当避开可能的混淆,给各个未命名的分组赋予名称,或者统一把它们变成非捕获分组。
名称相同的分组
Perl 5.10、Ruby 1.9和.NET允许多个命名捕获分组共享相同的名称。我们在实例4.5、8.7和8.19的解决方案中利用了这一特性。如果正则表达式使用选择分支来查找特定文本的不同变体,使用相同名称的捕获分组可以使提取匹配的子串更加容易,而不用关心哪个分支匹配了文本。实例4.5中“单纯正则表达式”一节使用选择分支分别匹配不同长度的月份日期。每个分支匹配一组日期和月份。因为在不同的分支中使用相同的“day”和“month”分组名称,我们只需要在正则表达式找到匹配后查询两个捕获分组即可获取日期和月份。
本书中其他支持命名捕获的正则流派将多个名称相同的分组视为错误。
多个捕获分组使用相同名称仅在只有一个分组参与匹配时可靠工作。本书中所有使用多个相同名称捕获分组的实例都是这种情况。各个分组处于不同选择分支中,而且选择分支不处于重复的分组中。Perl 5.10、Ruby 1.9和.NET的确允许两个相同名称的分组同时参与匹配。但反向引用的行为和匹配之后分组保存的文本在这些流派中各有不同。对我们来说,推荐在不同的选择分支中相同名称的分组就已经足够复杂了。