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

正则表达式子例程

Perl 5.10PCRE 4.0Ruby 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 使用的所有语法。最新版本的 PHPDelphiR 也支持所有这些语法,因为它们的正则表达式函数基于 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 个字母的字符串。带尖括号和带引号的语法可以互换使用。

JGsoft V2 支持所有三组语法。正如我们稍后将看到的,Perl、PCRE 和 Ruby 在子例程调用期间处理 捕获反向引用回溯 的方式存在差异。虽然它们互相复制了对方的语法,但它们并没有复制对方的行为。然而,JGsoft V2 复制了它们的语法和行为。因此,JGsoft V2 有三种不同的正则表达式递归方式,您可以通过使用不同的语法来选择。但是,这些差异不会影响本页上的基本示例。

Boost 1.42 从 Perl 复制了语法,但其实现存在缺陷,这些缺陷在 1.62 版本中仍未全部修复。最重要的是,除了 *{0,} 之外的量词会导致子例程调用出现异常。这在 Boost 1.60 中得到了部分修复,它正确地处理了 ?{0,1}

Boost 不支持 Ruby 语法用于子例程调用。在 Boost 中,\g<1> 是反向引用(不是子例程调用),用于捕获组 1。因此,([ab])\g<1> 可以匹配 aabb,但不能匹配 abba。在 Ruby 中,相同的正则表达式将匹配所有四个字符串。本教程中讨论的其他任何风格都不会将此语法用于反向引用。

匹配平衡结构

递归到捕获组中是一种比整个正则表达式的递归更灵活的匹配平衡结构的方法。我们可以将正则表达式包装在捕获组中,递归到捕获组而不是整个正则表达式,并在捕获组外添加锚点。\A(b(?:m|(?1))*e)\z 是用于检查字符串是否完全由正确平衡的结构组成的通用正则表达式。同样,b 是结构的开始,m 是可以在结构中间出现的内容,e 是可以在结构末尾出现的内容。为了获得正确的结果,bme 中的任何两个都不应该能够匹配相同的文本。你可以使用原子组而不是非捕获组来提高性能:\A(b(?>m|(?1))*e)\z

类似地,\Ao*(b(?:m|(?1))*eo*)+\z 和经过优化的 \Ao*+(b(?>m|(?1))*+eo*+)++\z 匹配一个字符串,该字符串仅由一个或多个正确平衡的结构序列组成,其间可能还有其他文本。此处,o 是可以在平衡结构之外出现的内容。它通常与 m 相同。o 不应该能够匹配与 be 相同的文本。

\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 和 JGsoft V2 中,你可以使用特殊 DEFINE 组进一步执行此操作:(?(DEFINE)(?'subroutine'regex))。虽然这看起来像一个条件,它引用了不存在的组 DEFINE,其中包含一个名为“subroutine”的单一命名组,但 DEFINE 组是一种特殊语法。固定文本(?(DEFINE)打开组。一个括号关闭组。此特殊组告诉正则表达式引擎忽略其内容,除了对其进行解析以查找命名和编号的捕获组。你可以在 DEFINE 组中放入任意数量的捕获组。DEFINE 组本身从不匹配任何内容,也从不匹配失败。它被完全忽略。正则表达式foo(?(DEFINE)(?'subroutine'skipped))bar匹配foobar。DEFINE 组在此正则表达式中完全多余,因为其中没有对任何组的调用。

使用 DEFINE 组,我们的正则表达式变为

(?(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]))
^Name:(.*)\r?\n
Born:(?P>date)\r?\n
Admitted:\ (?P>date)\r?\n
Released:\ (?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'$