本网站更多信息 |
简介 |
正则表达式快速入门 |
正则表达式教程 |
替换字符串教程 |
应用程序和语言 |
正则表达式范例 |
正则表达式参考 |
替换字符串参考 |
正则表达式子常式
Perl 5.10、PCRE 4.0 和 Ruby 1.9 支持正则表达式子常式调用。这些调用与正则表达式递归非常类似。子常式调用不会再次比对整个正则表达式,只会比对捕获组内的正则表达式。您可以在正则表达式的任何位置调用任何捕获组的子常式。如果您在子常式调用的群组内放置调用,您就会有一个递归捕获组。
与正则表达式递归一样,您可以使用各种语法来运行完全相同的事情。Perl 使用 (?1)
调用编号群组,(?+1)
调用下一个群组,(?-1)
调用前一个群组,以及 (?&name)
调用命名组。您可以使用所有这些调用来参照同一个群组。 (?+1)(?'name'[abc])(?1)(?-1)(?&name)
比对长度为五个字母,且仅包含字母表前三个字母的字符串。此正则表达式与 [abc](?'name'[abc])[abc][abc][abc]
完全相同。
PCRE 是第一个支持子程序调用的正则表达式引擎。 (?P<name>[abc])(?1)(?P>name)
匹配三个字母,例如 (?P<name>[abc])[abc][abc]
。 (?1)
是调用一个编号群组,而 (?P>name)
是调用一个命名组。后者在 PCRE 手册页中称为「Python 语法」。尽管此语法模仿 Python 用于 命名捕获组 的语法,但这是 PCRE 的发明。Python 不支持子程序调用或递归。PCRE 7.2 添加 (?+1)
和 (?-1)
以进行相对调用。PCRE 7.7 添加 Perl 5.10 和 Ruby 2.0 使用的所有语法。最新版本的 PHP、Delphi 和 R 也支持所有这些语法,因为它们的正则表达式函数是基于 PCRE。
Ruby 1.9 及之后版本使用的语法看起来更像反向引用。 \g<1>
和 \g'1'
调用一个编号群组,\g<name>
和 \g'name'
调用一个命名组,而 \g<-1>
和 \g'-1'
调用前一个群组。Ruby 2.0 添加 \g<+1>
和 \g'+1'
以调用下一个群组。 \g<+1>(?<name>[abc])\g<1>\g<-1>\g<name>
和 \g'+1'(?'name'[abc])\g'1'\g'-1'\g'name'
在 Ruby 2.0 中匹配与 Perl 范例在 Perl 中匹配的相同 5 个字母字符串。带有尖括号和引号的语法可以互换使用。
稍后我们将看到,Perl、PCRE 和 Ruby 在处理子程序调用期间的 截取、反向参考 和 回溯 方面存在差异。尽管它们拷贝了彼此的语法,但它们并没有拷贝彼此的行为。但这些差异不会在这个页面的基本范例中发挥作用。
Boost 1.42 从 Perl 拷贝了语法,但其实作却因错误而受损,这些错误在版本 1.62 中仍未完全修复。最重要的是,除了 *
或 {0,}
之外的量词会导致子程序调用行为异常。这在 Boost 1.60 中已获得部分修复,它也能正确处理 ?
和 {0,1}
。
Boost 不支持 Ruby 子程序调用的语法。在 Boost 中,\g<1>
是反向参考(不是子程序调用),用于捕获组 1。因此,([ab])\g<1>
可以匹配 aa
和 bb
,但不能匹配 ab
或 ba
。在 Ruby 中,相同的正则表达式将匹配所有四个字符串。本教程中讨论的其他风格都不使用此语法作为反向参考。
匹配平衡结构
递归进入捕获组是比递归整个 regex 更灵活的 配对平衡结构 方式。我们可以将 regex 包在捕获组中,递归进入捕获组而非整个 regex,并在捕获组外添加锚点。 \A(b(?:m|(?1))*e)\z
是用来检查字符串是否完全由正确平衡的结构组成的通用 regex。同样地,b
是结构的开头,m
是结构中间可能出现的内容,而 e
是结构结尾可能出现的内容。为了得到正确的结果,b
、m
和 e
这三者不应有两个以上可以配对相同的文本。你可以使用 原子组 取代 非捕获组 来提升性能:\A(b(?>m|(?1))*e)\z
。
类似地,\Ao*(b(?:m|(?1))*eo*)+\z
和优化的 \Ao*+(b(?>m|(?1))*+eo*+)++\z
会配对一个字符串,该字符串只包含一个或多个正确平衡的结构串行,中间可能穿插其他文本。在此,o
是平衡结构外可能出现的内容。它通常会与 m
相同。 o
不应能配对与 b
或 e
相同的文本。
\A(\((?>[^()]|(?1))*\))\z
符合一个字符串,该字符串仅包含一对平衡的括号,括号之间可能包含文本。 \A[^()]*+(\((?>[^()]|(?1))*+\)[^()]*+)++\z
。
比对同一结构超过一次
如果一个正则表达式需要在不同部分比对同一类型的结构(但不是完全相同的文本)超过一次,使用子常式调用会更简短且精简。假设您需要一个正则表达式来比对下列病历数据
Name: John Doe Born: 17-Jan-1964 Admitted: 30-Jul-2013 Released: 3-Aug-2013
此外,假设您需要准确比对日期格式,因此正则表达式可以筛选出有效的记录,并将无效记录留给人工检查。在大部分正则表达式风格中,您可以轻松使用 自由间距语法 通过这个正则表达式来运行此动作
^Name:\ (.*)\r?\n
Born:\ (?: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]\r?\n
Admitted:\ (?: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]\r?\n
Released:\ (?: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]$
通过子常式调用,您可以让这个正则表达式更短、更容易阅读和维护
^姓名:\ (.*)\r?\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])\r?\n
入院:\ \g'date'\r?\n
出院:\ \g'date'$
独立子程序定义
在 Perl 和 PCRE 中,您可以使用特殊 DEFINE 群组进一步运行此步骤:(?(DEFINE)(?'subroutine'regex))
。虽然这看起来像一个条件式,它参照不存在的群组 DEFINE,其中包含一个单一的名称群组「subroutine」,但 DEFINE 群组是一种特殊语法。固定文本(?(DEFINE)
打开群组。括号关闭群组。这个特殊组告诉 regex 引擎忽略其内容,除了解析它以取得已命名和已编号的捕获组。您可以将任意数量的捕获组放入 DEFINE 群组中。DEFINE 群组本身绝不匹配任何内容,而且绝不会无法匹配。它会被完全忽略。regex foo(?(DEFINE)(?'subroutine'skipped))bar
匹配foobar
。DEFINE 群组在此 regex 中完全多余,因为没有调用其内部的任何群组。
有了 DEFINE 群组,我们的 regex 会变成
(?(DEFINE)(?'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]))
^姓名:\ (.*)\r?\n
出生:\ (?P>date)\r?\n
入院:\ (?P>date)\r?\n
出院:\ (?P>date)$
子程序调用上的量词
子程序调用上的量词运作方式就像 递归上的量词。调用会重复运行,直到满足量词为止。 ([abc])(?1){3}
会配对 abcb
和任何其他由前三个字母组合而成的四个字母组合。首先,群组配对一次,然后调用配对三次。此正则表达式等于 ([abc])[abc]{3}
。
群组上的量词会被子程序调用忽略。 ([abc]){3}(?1)
也符合 abcb
。首先,群组符合三次,因为它有量词。然后子程序调用符合一次,因为它没有量词。 ([abc]){3}(?1){3}
符合六个字母,例如 abbcab
,因为现在群组和调用都重复 3 次。这两个正则表达式等于 ([abc]){3}[abc]
和 ([abc]){3}[abc]{3}
。
虽然 Ruby 不支持子程序定义群组,但它支持对重复零次的群组进行子程序调用。 (a){0}\g<1>{3}
符合 aaa
。群组本身被略过,因为它重复零次。然后子程序调用根据其量词符合三次。这也适用于 PCRE 7.7 及更新版本。它不适用于旧版本的 PCRE 或任何版本的 Perl,因为有错误。
病历范例的 Ruby 版本可以进一步清理为
(?'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]){0}
^姓名:\ (.*)\r?\n
出生:\ \g'date'\r?\n
入院:\ \g'date'\r?\n
出院:\ \g'date'$