教程 > 正则表达式 > 递归 阅读:37

子例程

正则表达式子例程


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也支持所有这些语法,因为它们的regex函数都是基于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示例匹配相同的5个字母字符串。带尖括号和引号的语法可以互换使用。

JGsoft V2支持所有三组语法。稍后我们将看到,Perl,PCRE和Ruby在子例程调用期间,对于capture,backreferences和backtracking的处理是有所不同的。当他们复制彼此的语法时,他们没有复制彼此的行为。但是,JGsoft V2复制了它们的语法和行为。因此,JGsoft V2具有三种执行正则表达式递归的方法,我们可以使用不同的语法来选择它们。但是,这些差异不会在此页面的基本示例中展现出来。

Boost 1.42从Perl复制了语法,但其实现的过程中是有bug的,而bug在1.62版本中仍未全部修复。最重要的是,对于不是*或{0,}的量词会导致子例程调用行为异常。这在Boost 1.60中已部分修复,可以正确处理?和{0,1}。

Boost不支持用于子例程调用的Ruby语法。在Boost中\g<1>是捕获组1的反向引用(而不是子例程调用)。因此([ab])\g<1>可以匹配aa和bb,但不能匹配ab或ba 。在Ruby中,相同的正则表达式将匹配aa,bb,ab和ba。其他的正则表达式引擎都不会将此语法用于反向引用

匹配平衡结构


与整个正则表达式的递归相比,递归到捕获组是匹配平衡结构的一种更灵活的方式。我们可以将正则表达式封装在捕获组中,递归捕获组,而不是整个正则表达式中,然后在捕获组外添加锚点。\A(b(?:m|(?1))*e)\z是通用正则表达式,用于检查字符串是否完全由平衡的结构组成。同样,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

进一步假设我们需要相当准确地匹配日期格式,以便正则表达式可以过滤掉无效记录,从而筛选出有效记录留给人们去检查。在大多数regex中,可以使用free-spacing语法轻松地通过下面的正则表达式进行此操作:

^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]$

通过子例程调用,我们可以使此正则表达式更短,更易于阅读且更易于维护:

^Name:\ (.*)\r?\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])\r?\n
Admitted:\ \g'date'\r?\n
Released:\ \g'date'$               
```               
### 单独的子例程定义
***
在Perl,PCRE和JGsoft V2,我们可以通过下面的语法更进一步使用特殊的DEFINE组:`(?(DEFINE)(?'subroutine'regex))`。尽管这看起来像是一个条件组,它引用了不存在的组DEFINE,该组包含单个命名组“ subroutine”,但DEFINE组是一种特殊的语法。固定文本`(?(DEFINE)` 打开该组。右括号关闭该组。此特殊组告诉正则表达式引擎忽略其内容,而不是解析命名和编号的捕获组。我们可以在其中放入尽可能多的捕获组定义组只要我们喜欢。定义组本身永远不匹配任何东西,从来不匹配。它是完全忽略的。正则表达式`foo(?(DEFINE)(?'subroutine'skipped))bar`匹配foobar。定义组在此正则表达式中完全多余,因为其中没有对内部任何组的调用。

使用DEFINE组,我们的正则表达式变为:
```bash
(?(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和由a,b和c组成的任意一个长度为4的字符串。首先,组匹配一次,然后调用匹配三遍。此正则表达式等效于`([abc])[abc]{3}`。

子例程调用将忽略组中的量词。`([abc]){3}(?1)`也匹配abcb 。首先,该组匹配三遍,因为它有一个量词{3}。然后,子例程调用将匹配一次,因为它没有量词。`([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版本可以进一步整理为:
```bash
(?'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}
^Name:\ (.*)\r?\n
Born:\ \g'date'\r?\n
Admitted:\ \g'date'\r?\n
Released:\ \g'date'$  

         

查看笔记

扫码一下
查看教程更方便