本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.17节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.17 根据条件匹配两者之一
问题描述
创建一个正则表达式,匹配一个由逗号分隔的单词列表one、two和three。每个单词可以在该列表中出现任意多次,但是每个单词必须至少出现一次。
解决方案
\b(?:(?:(one)|(two)|(three))(?:,|\b)){3,}(?(1)|(?!))(?(2)|(?!))(?(3)|(?!))
正则选项:无
正则流派:.NET、PCRE、Perl、Python
Java、JavaScript和Ruby并不支持条件判断。在这些(或者其他任何语言)中进行编程的时候,你可以使用不带有条件判断的正则表达式,再另外编写一些其他的代码来检查其中的三个捕获分组是否都匹配了相应的内容。
\b(?:(?:(one)|(two)|(three))(?:,|\b)){3,}
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
.NET、PCRE、Perl和Python支持使用编号捕获分组的条件判断(conditional)。‹(?(1)then|else)›是用来检查第一个捕获分组是否成功匹配的一个条件判断。如果它匹配成功,正则引擎会尝试去匹配‹then›。如果该捕获分组到目前为止还没有参与匹配尝试,就会去尝试匹配‹else›。
这里的括号、问号和竖线都是属于条件判断语法的一部分。它们在这里并不具有平时的含义。你可以在‹then›和‹else›部分中使用任意种类的正则表达式。唯一的限制是如果想要在其中一个部分之内使用选择分支,就必须使用分组来把它们包到一起。因为在条件判断中只允许直接出现一条竖线。
如果愿意,也可以省略掉‹then›或‹else›的部分。空的正则表达式总是会找到一个长度为0的匹配。这个实例所给的解决方案中使用了3个条件判断,它们都包含空的‹then›部分。如果捕获分组参与了匹配,那么这个条件判断只会简单地成功匹配。
一个空否定型顺序环视‹(?!)›用在了‹else›部分。因为空的正则表达式总是会成功匹配,所以包含空正则表达式的否定型顺序环视则总是会匹配失败。因此,如果第一个捕获分组没有匹配到任何东西,条件判断‹(?(1)|(?!))›必定会失败。
通过把这三个必需的选择分支放到自己的捕获分组中,我们可以在正则表达式的结尾使用3个条件判断来检查是否所有的捕获分组都捕获到了内容。如果其中一个单词没有匹配,引用其捕获分组的条件判断就会执行“else”部分,该部分的空否定型顺序环视就会导致条件判断匹配失败。因此只要有一个单词没有匹配,整个正则式就没有匹配成功。
要允许单词以任意顺序出现,并且出现任意次数,我们将所有单词放在一个使用选择分支的分组内,并使用量词重复这个分组。因为我们有三个单词,并且要求每个单词至少匹配一次,所以我们知道这个分组至少要重复三次。.NET、Python和PCRE 6.7还支持命名捕获分组的条件判断。‹(?(name)then|else)›会检查命名捕获分组name是否参与了匹配尝试。Perl 5.10及以后版本同样支持命名条件判断。但是Perl要求名字两边使用尖括号或单引号,如‹(?(< name >)then|else)›或‹(?('name')then|else)›。PCRE 7.0及以后版本也支持Perl的命名条件判断语法,以及.NET和Python所采用的语法。
为了更好地理解条件判断是如何工作的,我们来看一个正则表达式‹(a)?b(?(1)c|d)›。它本质上是与‹abc|bd›等价的一种更为复杂的形式。
如果目标字符串是由一个a开头的,那么它就会被捕获到第一个捕获分组中。如果不是,那么第一个捕获分组就没有参与到匹配尝试中。在该捕获分组之后的问号是很重要的,因为这使得整个分组成为可选的。如果不存在a的话,该分组会重复0次,因此不会有机会捕获任何内容。它不能捕获一个长度为0的字符串。
如果你使用的是‹(a?)›,那该分组总是会参与到匹配尝试中。而在该分组之后并不存在量词,所以它会正好重复一次。该分组或者捕获a,或者捕获空串。
不管‹a›是否成功匹配,下一个记号是‹b›。然后是条件判断。如果捕获分组参与了匹配尝试,即使它捕获的是长度为0的字符串(在这里是不可能的),都会尝试匹配‹c›。如果没有的话,那么会尝试匹配‹d›。
用一句话来描述,‹(a)?b(?(1)c|d)›或者匹配ab后跟着c,或者匹配b后跟着d。
在.NET、PCRE和Perl中(但是不包括Python),条件判断中还可以使用环视。‹(?(?=if__)then|else)›首先会把‹(?=if )›当作一个正常的顺序环视来进行测试。实例2.16中讲解了它是如何执行的。如果环视匹配成功的话,那么会接着尝试匹配‹then›部分。如果没有成功的话,那么会尝试匹配‹else›中的正则表达式。因为环视的长度为0,所以‹then›和‹else›中的正则表达式都会在目标文本中‹if›匹配成功或者失败的同一位置处接着进行尝试。
在条件判断中,除了使用顺序环视,也可以使用逆序环视。你还可以使用否定型的环视,虽然我们并不推荐这样做,因为它会把“then”和“else”的含义反转,从而造成不必要的混淆。
图像说明文字使用环视的条件判断也可以写成不使用条件判断的形式:‹(?=if)then| (?!if)else›。如果肯定型顺序环视匹配成功,就会尝试匹配‹then›部分。如果肯定型顺序环视匹配失败,那么会尝试匹配第二个选择。接下来的否定型顺序环视会做同样的检查。否定型顺序环视在‹if›匹配失败的时候会匹配成功,因为‹(?=if)›已经匹配失败了,所以否定型顺序环视肯定成功。因此,继续尝试匹配‹else›。把顺序环视放到条件判断中更节省时间,因为条件判断只需尝试匹配‹if›一次。