快速入门
教程
工具和语言
示例
参考
书评
正则表达式教程
简介
目录
特殊字符
不可打印字符
正则表达式引擎内部
字符类
字符类减法
字符类交集
简写字符类
锚点
词边界
交替
可选项目
重复
分组和捕获
反向引用
反向引用,第 2 部分
命名组
相对反向引用
分支重置组
自由间距和注释
Unicode
模式修饰符
原子分组
占有量词
前瞻和后顾
环视,第 2 部分
将文本排除在匹配之外
条件
平衡组
递归
子例程
无限递归
递归和量词
递归和捕获
递归和反向引用
递归和回溯
POSIX 方括号表达式
零长度匹配
继续匹配
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书评
可打印的 PDF
关于本网站
RSS 源和博客
RegexBuddy—Better than a regular expression tutorial!

子例程调用可能捕获,也可能不捕获

本教程介绍了正则表达式子例程,其中包含我们想要准确匹配的示例

Name: John Doe
Born: 17-Jan-1964
Admitted: 30-Jul-2013
Released: 3-Aug-2013

RubyPCRE中,我们可以使用此正则表达式

^姓名:(.*)\n
出生:(?'date'(?:3[01]|[12][0-9]|[1-9])
               
-(?:1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月)
               
-(?:19|20)[0-9][0-9])\n
入院:\g'date'\n
出院:\g'date'$

Perl 需要稍有不同的语法,这在 PCRE 中也有效

^姓名:(.*)\n
出生日期:(?'date'(?:3[01]|[12][0-9]|[1-9])
               
-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
               
-(?:19|20)[0-9][0-9])\n
入院日期:(?&date)\n
出院日期:\ (?&date)$

不幸的是,除了语法之外,这三种正则表达式风格在处理子例程调用时存在差异。首先,在 Ruby 中,子例程调用会使捕获组存储在子例程调用期间匹配的文本。在 Perl、PCRE 和 Boost 中,子例程调用不会影响被调用的组。

当 Ruby 解决方案匹配上述示例时,检索捕获组“date”的内容将得到 3-Aug-2013,这是由对该组的最后一个子例程调用匹配的。当 Perl 解决方案匹配相同的内容时,检索 $+{date} 将得到 17-Jan-1964。在 Perl 中,子例程调用根本没有捕获任何内容。但是,“Born”日期与一个普通的 命名捕获组 匹配,该组正常存储了它匹配的文本。对该组的任何子例程调用都不会改变这一点。在这种情况下,PCRE 的行为与 Perl 相同,即使您对 PCRE 使用 Ruby 语法也是如此。

JGsoft V2 在使用第一个正则表达式时表现得像 Ruby。你可以通过 \g 语法是 Ruby 发明,之后被 PCRE 复制这一事实来记住这一点。JGsoft V2 在使用第二个正则表达式时表现得像 Perl。你可以通过 Perl 在过程代码中也使用 & 符号进行子例程调用这一事实来记住这一点。

如果你想从匹配项中提取日期,最好的解决方案是为每个日期添加另一个捕获组。然后你可以忽略“date”组存储的文本,以及这些风格之间的这种特定差异。在 Ruby 或 PCRE 中

^姓名:(.*)\n
出生:(?'born'(?'date'(?:3[01]|[12][0-9]|[1-9])
                       
-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
                       
-(?:19|20)[0-9][0-9]))\n
入院:(?'admitted'\g'date')\n
出院:(?'released'\g'date')$

Perl 需要稍有不同的语法,这在 PCRE 中也有效

^姓名:(.*)\n
出生日期:(?'born'(?'date'(?:3[01]|[12][0-9]|[1-9])
                       
-(?:1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月)
                       
-(?:19|20)[0-9][0-9]))\n
入院日期:(?'admitted'(?&date))\n
出院日期:\ (?'released'(?&date))$

在递归或子例程调用中捕获组

当你的正则表达式对包含其他捕获组的捕获组进行子例程调用或递归调用时,Perl、PCRE 和 Ruby 之间存在进一步的差异。如果正则表达式包含任何捕获组,则相同的问题也会影响整个正则表达式的递归。对于本主题的其余部分,“递归”一词同样适用于整个正则表达式的递归、对捕获组的递归或对捕获组的子例程调用。

PCRE 和 Boost 在进入和退出递归时备份并恢复捕获组。当正则表达式引擎进入递归时,它会在内部复制所有捕获组。这不会影响捕获组。递归中的反向引用匹配递归之前捕获的文本,除非且直到它们引用的组在递归期间捕获到某些内容。递归之后,所有捕获组都将替换为递归开始时制作的内部副本。在递归期间捕获的文本将被丢弃。这意味着你不能使用捕获组来检索在递归期间匹配的文本部分。

从 5.10 版(第一个具有递归功能的版本)到 5.18 版,Perl 5 在每次递归级别之间隔离捕获组。当 Perl 5.10 的正则表达式引擎进入递归时,所有捕获组都显示为尚未参与匹配。最初,所有反向引用都将失败。在递归期间,捕获组正常捕获。反向引用正常匹配在同一递归期间捕获的文本。当正则表达式引擎退出递归时,所有捕获组都恢复到递归之前的状态。Perl 5.20 更改了 Perl 的行为,以 PCRE 的方式备份和恢复捕获组。

然而,对于大多数实际目的,你只会在其对应的捕获组之后使用反向引用。然后,Perl 5.10 到 5.18 在递归期间处理捕获组的方式与 PCRE 和更高版本的 Perl 的方式之间的差异是学术性的。

Ruby 的行为完全不同。当 Ruby 的正则表达式引擎进入或退出递归时,它根本不会更改捕获组存储的文本。反向引用匹配组最近一次匹配期间捕获组存储的文本,而不管可能发生的任何递归。在找到整体匹配后,每个捕获组仍存储其最近一次匹配的文本,即使那是在递归期间。这意味着你可以使用捕获组来检索在最后一次递归期间匹配的文本部分。

当你使用从 Ruby 借用的 \g 语法时,JGsoft V2 的行为与 Ruby 相同。当你使用任何其他语法时,它的行为与 Perl 5.20 和 PCRE 相同。

Perl 和 PCRE 中的奇数长度回文

在 Perl 和 PCRE 中,您可以使用 \b(?'word'(?'letter'[a-z])(?&word)\k'letter'|[a-z])\b 来匹配回文单词,例如 adadradarracecarredivider。此正则表达式仅匹配长度为奇数个字母的回文单词。这涵盖了英语中的大多数回文单词。要扩展正则表达式以处理长度为偶数个字符的回文单词,我们必须担心 Perl 和 PCRE 在 递归尝试失败后回溯 方式上的差异,本教程稍后会讨论。我们在这里忽略这些差异,因为它们仅在主题字符串不是回文且找不到匹配项时才会发挥作用。

让我们看看此正则表达式如何匹配 radar单词边界 \b 匹配字符串的开头。正则表达式引擎进入两个捕获组。[a-z] 匹配 r,然后将其存储在捕获组 “letter” 中。现在,正则表达式引擎进入组 “word” 的第一次递归。此时,Perl 忘记了 “letter” 组匹配了 r。PCRE 不会忘记。但这并不重要。(?'letter'[a-z]) 匹配并捕获 a。正则表达式进入组 “word” 的第二次递归。(?'letter'[a-z]) 捕获 d。在接下来的两次递归中,该组捕获 ar。第五次递归失败,因为字符串中没有字符供 [a-z] 匹配。正则表达式引擎必须回溯。

由于 (?&word) 匹配失败,(?'letter'[a-z]) 必须放弃其匹配。该组恢复为 a,这是该组在递归开始时保存的文本。(在 Perl 5.18 及更早版本中,它将变为空。)同样,这并不重要,因为正则表达式引擎现在必须尝试组“word”中的第二个备选方案,其中不包含反向引用。第二个 [a-z] 匹配字符串中的最后一个 r。引擎现在退出成功的递归。组“letter”存储的文本恢复为在进入第四次递归之前捕获的内容,即 a

在匹配 (?&word) 之后,引擎到达 \k'letter'。反向引用失败,因为正则表达式引擎已经到达主题字符串的末尾。因此它再次回溯,使捕获组放弃 a。第二个备选方案现在匹配 a。正则表达式引擎退出第三次递归。组“letter”恢复为在第二次递归期间匹配的 d

正则表达式引擎再次匹配 (?&word)。反向引用再次失败,因为该组存储 d,而字符串中的下一个字符是 r。再次回溯,第二个备选方案匹配 d,并且该组恢复为在第一次递归期间匹配的 a

现在,\k'letter' 匹配字符串中的第二个 a。这是因为正则表达式引擎已返回到第一个递归,在此期间捕获组匹配了第一个 a。正则表达式引擎退出第一个递归。捕获组恢复到第一个递归之前匹配的 r

最后,反向引用匹配第二个 r。由于引擎不再处于任何递归中,因此它将继续执行组之后的正则表达式的其余部分。 \b 匹配字符串结尾。正则表达式的结尾已达到,radar 作为整体匹配返回。如果您在匹配后查询组“word”和“letter”,您将获得 radarr。这是所有递归之外这些组匹配的文本。

此正则表达式为何在 Ruby 中不起作用

要在 Ruby 中以这种方式匹配回文,您需要使用特殊 指定递归级别的反向引用。如果您使用普通反向引用,如 \b(?'word'(?'letter'[a-z])\g'word'\k'letter'|[a-z])\b,Ruby 不会抱怨。但它也不会匹配长度超过三个字母的回文。相反,此正则表达式匹配 adadradaaracecccrediviiii 等内容。

让我们看看为什么此正则表达式在 Ruby 中不匹配 radar。Ruby 的开头与 Perl 和 PCRE 相同,进入递归,直到字符串中没有字符可供 [a-z] 匹配。

由于 \g'word' 匹配失败,(?'letter'[a-z]) 必须放弃其匹配。Ruby 将其还原为 a,这是该组最近匹配的文本。第二个 [a-z] 匹配字符串中的最后一个 r。引擎现在退出成功的递归。组“letter”继续保留其最近的匹配 a

在匹配 \g'word' 后,引擎到达 \k'letter'。回引失败,因为正则引擎已经到达主题字符串的末尾。因此它再次回溯,将组还原为先前匹配的 d。第二个选项现在匹配 a。正则引擎退出第三次递归。

正则引擎再次匹配 \g'word'。回引再次失败,因为组存储 d,而字符串中的下一个字符是 r。再次回溯,组还原为 a,第二个选项匹配 d

现在,\k'letter' 匹配字符串中的第二个 a。正则引擎退出成功匹配 ada 的第一次递归。捕获组继续持有 a,这是它最近一次未回溯的匹配。

正则引擎现在位于字符串中的最后一个字符。此字符为 r。回引失败,因为组仍然持有 a。引擎可以再次回溯,强制 (?'letter'[a-z])\g'word'\k'letter' 放弃它迄今为止匹配的 rada。正则引擎现在返回到字符串的开头。它仍然可以尝试组中的第二个选项。这匹配字符串中的第一个 r。由于引擎不再处于任何递归中,因此它继续执行组之后的正则表达式其余部分。\b 在第一个 r 后不匹配。正则引擎没有进一步的排列可尝试。匹配尝试失败。

如果主题字符串是 radaa,Ruby 的引擎会经历几乎与上面描述相同的匹配过程。只有最后一段中描述的事件会改变。当正则表达式引擎到达字符串中的最后一个字符时,该字符现在是 a。这一次,反向引用匹配。由于引擎不再处于任何递归中,因此它将继续处理组后面的正则表达式的其余部分。 \b 在字符串的末尾匹配。正则表达式的末尾已到达,radaa 作为整体匹配返回。如果在匹配后查询组“word”和“letter”,您将获得 radaaa。这些是这些组最近的匹配,没有回溯。

基本上,在此 Ruby 中,此正则表达式匹配任何长度为奇数个字母的单词,其中中间字母右侧的所有字符都与中间字母左侧的字符相同。这是因为 Ruby 仅在回溯时恢复捕获组,而不在退出递归时恢复捕获组。

针对 Ruby 的解决方案是使用 指定递归级别的反向引用,而不是本页正则表达式中使用的普通反向引用。