条件子组
正则表达式中的If-Then-Else条件
特殊的构造(?ifthen|else)
允许我们创建条件正则表达式。如果if部分评估为true,则正则表达式引擎将尝试匹配then部分。否则,尝试使用else部分。语法由一对括号组成。左括号必须跟一个问号,紧接着是if部分,然后是then部分。这部分后面可以有一个竖线和else部分。我们可以省略其他部分,以及竖线。
对于if部分,可以使用lookahead和lookbehind构造。使用肯定前瞻,语法变为(?(?=regex)then|else)
。因为前瞻有自己的括号,所以if和then部分被清楚地分开了。
请记住,lookaround不占用任何字符。如果将前瞻部分用作if部分,则正则表达式引擎将尝试在if所在的相同位置匹配then或else部分(取决于前瞻结果)。
或者,我们可以在if部分检查到目前为止捕获组是否已参加匹配。将捕获组的编号放在括号内,并将其用作if部分。请注意,尽管对反向引用进行条件检查的语法与捕获组中的数字相同,但不会创建捕获组。数字和括号是以(?
开头的if-then-else语法的一部分。
对于then和else ,可以使用任何正则表达式。如果要使用选择项,则必须使用括号将then或else分组在一起,例如: (?(?=condition)(then1|then2|then3)|(else1|else2|else3))
。否则,无需在then和else部分周围使用括号。
瞧瞧正则表达式引擎内部
正则表达式(a)?b(?(1)c|d)
由可选的捕获组(a)?
,文字b和测试捕获组的条件(?(1)c|d)
组成。此正则表达式匹配bd和abc 。它不匹配bc ,但是匹配字符串abd中的bd。让我们看看这个正则表达式如何在这四个目标字符串中的每一个上面工作的。
当应用于bd时,a匹配失败。由于包含a的捕获组是可选的,因此引擎在目标字符串的开头以b继续。由于整个小组是可选的,因此该小组没有参加匹配。任何后续的反向引用,例如\1
都将失败。请注意(a)?
与(a?)
有很大的不同。前者的正则表达式,捕获组不参加匹配,如果a失败,反向引用将失败。在后一组中,捕获组始终存在,匹配到a或没有。参与匹配但未捕获任何内容的捕获组的反向引用始终会成功。评估这些组的条件执行“ then”部分。简而言之:如果要在条件中使用对组的引用,要使用(a)?
而不是(a?)
。
继续我们的正则表达式,b
匹配b 。正则表达式引擎现在评估条件。第一个捕获组根本没有参加匹配,因此尝试“其他”部分或d 。d匹配d ,找到一个整体匹配项。
再来看第二个目标字符串abc ,正则标记a匹配字符a宾切由捕获组捕获。随后,b匹配b 。正则表达式引擎再次评估条件。捕获组参加了匹配,因此尝试了“ then”部分c 。正则标记c匹配字符c,找到一个整体匹配项。
我们的第三个目标字符串bc并非以a开头,因此捕获组不会参与匹配尝试,就像我们在第一个主题字符串中看到的那样。b仍然与b匹配,并且引擎继续执行条件引擎。第一个捕获组根本没有匹配,因此尝试“else”部分d 。d与c不匹配,并且在字符串开头的匹配尝试失败。引擎确实从字符串中的第二个字符开始再次尝试,但是由于正则标记b与字符c不匹配而失败。
第四个目标字符串abd是最有趣的。像第二个字符串一样,捕获组获取a和b匹配项。捕获组匹配成功,因此尝试了“ then”部分c 。c未能匹配字符d ,并且匹配尝试失败。请注意,此时不尝试“ else”部分。捕获组匹配成功,因此仅使用“ then”部分。但是,正则表达式引擎尚未完成。它从头开始重新启动正则表达式,在目标字符串中向前移动一个字符。
从字符串的第二个字符开始,a不能匹配b 。捕获组不参与从字符串中第二个字符开始的第二次匹配尝试。正则表达式引擎移到可选组之外,并尝试匹配的b 。正则表达式引擎现在到达正则表达式中的条件语句和主题字符串中的第三个字符。第一个捕获组没有参加当前的匹配尝试,因此尝试了“其他”部分d 。正则标记d匹配字符d,找到一个整体匹配bd 。
如果要避免最后一个匹配结果,则需要使用anchors。^(a)?b(?(1)c|d)$
在最后一个目标字符串中找不到任何匹配项。^
在字符串的第二个和第三个字符之前无法匹配。
命名和相对条件
JGsoft引擎,Perl,PCRE,Python和.NET框架支持条件语句。Ruby从2.0版本开始支持它们。具有基于PCRE的正则表达式功能的语言(如Delphi,PHP和R)也支持条件语句。
所有这些也都支持命名捕获组。我们可以将捕获组的名称而不是其编号用作if测试。这里在各种正则表达式引擎之间的语法略有不一致。在Python,.NET和JGsoft应用程序中,只需在括号之间指定组的名称。(?<test>a)?b(?(test)c|d)
是上一节使用命名捕获的正则表达式。在Perl或Ruby中,必须将尖括号或引号放在组名的周围,并将其放在条件的括号之间:(?<test>a)?b(?(<test>)?c|d)
或者(?'test'a)?b(?('test')c|d)
。PCRE支持这三种方式。
有条件的引用不存在的捕获组
Boost和Ruby将引用不存在的捕获组的条件视为错误。本教程中讨论的所有其他正则引擎的最新版本都没有。他们只是让这种条件总是尝试“else”部分。不过,有些正则引擎改变了他们的想法。使用Python 3.4和更低版本以及PCRE 7.6和更高版本(以及PHP 5.2.5和更高版本)将它们视为错误。
示例:提取电子邮件标题
正则表达式^((From|To)|Subject):((?(2)\w+@\w+\.[a-z]+|.+))
从电子邮件中提取From,To和Subject标头。标头的名称被捕获到第一个反向引用中。如果标题是From或To标题,则它也将捕获到第二个反向引用中。
模式的第二部分是if-then-else条件(?(2)\w+@\w+\.[a-z]+|.+))
。if部分检查迄今为止第二捕获组是否在参与了匹配。如果标题是From或To标题,它将参与其中。在这种情况下,则该条件的一部分\w+@\w+.[a-z]+尝试匹配电子邮件地址。为了使示例保持简单,我们使用了一个过于简单的正则表达式来匹配电子邮件地址,并且我们不尝试匹配通常也是From或To标题的一部分的显示名称。
如果第二个捕获组到目前为止还没有参加匹配,那么将尝试使用else部分.+
代替。这仅与该行的其余部分匹配,从而允许任何测试对象。
最后,我们在条件语句的周围放置了一对额外的括号。这会将与条件匹配的电子邮件标题的内容捕获到第三个反向引用中。条件本身不捕获任何内容。在实现此正则表达式时,第一个捕获组将存储标头的名称(“ From”,“ To”或“ Subject”),而第三个捕获组将存储标头的值。
我们可以通过将另一个条件放入“else”部分来尝试匹配更多的标头。例如
^((From|To)|(Date)|Subject):((?(2)\w+@\w+\.[a-z]+|(?(3)mm/dd/yyyy|.+))))
会匹配“From”,“To”,“Date”或“Subject”,并使用正则表达式mm/dd/yyyy
来检查日期是否有效。显然,日期验证正则表达式只是为了使示例简单而已。标头是在第一组中捕获的,其验证的内容是在第四组中捕获的。
如我们所见,使用条件的正则表达式很快变得笨拙。我建议仅在我们的工具允许使用一个正则表达式的情况下才使用它们。进行编程时,最好使用regex ^(From|To|Date|Subject):(.+)
捕获一个带有未验证内容的标头。在我们的源代码中,检查在第一个捕获组中返回的标头的名称,然后使用第二个正则表达式来验证在第一个正则表达式的第二个捕获组中返回的标头的内容。尽管我们必须编写几行额外的代码,但是这些代码将更易于理解和维护。如果我们预编译所有正则表达式,则使用多个正则表达式将与填充条件的一个大正则表达式一样快,甚至更快。