《正则表达式经典实例(第2版)》——2.16 测试一个匹配,但不添加到整体匹配中

简介: 匹配的过程则是完全相同的。引擎会在进入否定型顺序环视的时候保存当前匹配进程,然后试图正常地匹配顺序环视中的正则表达式。如果这个子表达式匹配的话,那么否定型顺序环视会失败,而正则引擎会进行回溯。如果这个子表达式不能匹配的话,那么引擎会恢复保存的匹配进程,然后继续处理正则表达式的剩余部分。

本节书摘来自异步社区《正则表达式经典实例(第2版)》一书中的第2章,第2.16节,作者: 【美】Jan Goyvaerts , Steven Levithan著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.16 测试一个匹配,但不添加到整体匹配中

问题描述
找出在一对HTML粗体标签之间的任何单词,但是不要把标签包含到正则表达式匹配中。例如,如果目标文本是My cat is furry,那么唯一的匹配应当是cat。

解决方案

(?<=<b>)\w+(?=</b>)
正则选项:不区分大小写
正则流派:.NET、Java、PCRE、Perl、Python、Ruby 1.9

JavaScript和Ruby 1.8支持顺序环视(lookahead)‹(?=)›,但是不支持逆序环视(lookbehind)‹(?<=< b >)›。

讨论
环视
现代的正则流派都支持四种类型的环视(lookaround),它们拥有特殊的能力,可以放弃在环视内部的正则表达式所匹配的文本。实质上,环视会检查某些文本是否可以被匹配,但是并不会实际去匹配它。

向回看的环视被称作是逆序环视。这是唯一可以从右向左而非从左向右遍历目标文本的正则表达式结构。肯定型逆序环视(positive lookbehind)的语法是‹(?<= ⋯)›。‹(?<=›4个字符构成了起始括号。你能在逆序环视内部放入什么内容(这里由‹⋯›表示)在不同正则表达式流派中是不一样的。但是简单的字面文本总是没问题的,如‹(?<=< b >)›。

逆序环视会检查在逆序环视中的文本是否直接出现在正则表达式引擎所到达位置的左边。如果用‹(?<=< b >)›来匹配My < b >cat b > is furry,只有到正则表达式在目标文本中的字母c处开始进行匹配尝试时,逆序环视才会匹配成功。正则引擎接着会进入逆序环视分组,告诉它向左边看。‹< b >›在c的左边成功匹配。正则引擎会在这个时候退出逆序环视,并且丢弃逆序环视所匹配到的任何文本。换句话说,正在进行的匹配会回到引擎刚刚进入逆序环视的地方。在这个例子中,正在进行匹配的是目标字符串中c之前的一个长度为0的匹配。逆序环视只会测试或者断言‹< b >›是否可以被匹配;但是它并不会实际上去匹配它。环视结构因此也被称作长度为0的断言。

在逆序环视匹配之后,字符组简写‹w+›会尝试去匹配一个或者多个单词字符。它会匹配cat。‹w+›并不属于任何类型的环视或者分组,因此它会正常地匹配文本cat。我们说‹w+›匹配并且消耗(consume)了cat,而环视则只能匹配内容,却从来不会消耗任何东西。

向前看的环视,也就是按照正则表达式通常遍历文本的方向,被称作顺序环视(lookahead)。顺序环视在本书中的所有正则流派中都拥有同等的支持。肯定型顺序环视(positive lookahead)的语法是‹(?=⋯)›。这3个字符‹(?=›构成了该分组的起始括号。在一个正则表达式中可以使用的任何符号都可以在顺序环视内部使用,在这里用‹⋯›来表示。

当‹(?<=< b >)w+(?=< /b >)›中的‹w+›匹配了My < b >cat b > is furry中的cat的时候,正则引擎就进入了顺序环视。在这个时候顺序环视唯一特殊的行为是正则引擎会记住它已经匹配了的文本部分,并把它同顺序环视关联起来。‹ b >›随后会正常匹配。现在正则引擎退出顺序环视。在环视中的正则表达式成功匹配,因此环视自身也就匹配成功了。正则引擎还原在进入环视之前它记住的正在进行的匹配,从而丢弃由环视匹配的文本。这样我们整体上的匹配进程就回到了cat。因为我们正则表达式也在此结束,所以cat就成为了最终的匹配结果。

否定型环视
把环视中的等号换成感叹号的话,‹(?!⋯)›就变成了否定型顺序环视(negative lookahead)。否定型顺序环视与肯定型顺序环视用起来是一样的,唯一的区别是,肯定型顺序环视会在顺序环视中的正则式匹配时成功匹配,而否定型顺序环视则正好相反,它在当顺序环视内的正则式匹配时,匹配失败。

匹配的过程则是完全相同的。引擎会在进入否定型顺序环视的时候保存当前匹配进程,然后试图正常地匹配顺序环视中的正则表达式。如果这个子表达式匹配的话,那么否定型顺序环视会失败,而正则引擎会进行回溯。如果这个子表达式不能匹配的话,那么引擎会恢复保存的匹配进程,然后继续处理正则表达式的剩余部分。

类似的,‹(?

不同层次的逆序环视
顺序环视用起来比较容易。本书中讨论的所有正则流派都支持在顺序环视中放入一个完整的正则表达式。在正则表达式中可以使用的任何符号都可以用于顺序环视之内。你甚至可以在顺序环视内嵌套其他顺序环视和逆序环视分组。你的大脑可能需要多绕几个弯,但是正则引擎会把这一切都处理得很好。

逆序环视的情况则不同。正则表达式软件总是设计成按照从左向右的方式查找目标文本。向回查找的实现通常需要一些特殊的处理:正则引擎会判断你在逆序环视中输入了多少个字符,回退那么多数量的字符,然后再在目标文本中从左向右匹配位于逆序环视中的文本。

基于这个原因,最早的实现中只允许在逆序环视中包含固定长度的字面文本。尽管Perl和Python仍需要逆序环视拥有固定的长度,但它们已经允许固定长度的正则表达式记号,如字符组,以及所有分支字符数都相同的选择分支。

PCRE和Ruby 1.9则更进一步,允许逆序环视中使用不同分支长度的选择分支,只要各分支的长度是不变的。它们可以处理类似如下的正则式:‹(?<=one|two|three|forty- two|gr[ae]y)›,但是无法处理更为复杂的情况。

PCRE和Ruby 1.9在内部会把这个表达式扩展为6个逆序环视测试。首先,它们会回跳3个字符来测试‹one|two›,接着回跳4个字符来测试‹gray|grey›,然后回跳5个字符来测试‹three›,最后回跳9个字符测试‹forty-two›。

Java对于逆序环视则更进一步。Java允许在逆序环视中使用任意的有限长度的正则表达式。这意味着你可以使用除了无限长度量词‹*›、‹+›和‹{42,}›之外的所有符号。Java的正则引擎在内部会计算在逆序环视中的正则表达式可能会匹配的文本的最小和最大长度。如果它匹配失败的话,那么引擎会多回退一个字符再试,直到逆序环视成功匹配或者尝试过了最大字符数目。

似乎这些听起来都不是很高效,事实上也正是如此。逆序环视用起来是非常方便的结构,但是它的速度就很一般了。稍后,我们会讲解在根本不支持逆序环视的JavaScript和Ruby 1.8中的一个解决方案。这个解决方案实际上会比使用逆序环视的效率要高很多。

.NET框架中的正则表达式引擎是唯一可以实际上从右向左应用一个完整正则表达式的引擎1FF。.NET允许在逆序环视中使用任何符号,而且它会实际上从右向左来应用正则表达式。在逆序环视中的正则表达式和目标文本都是按照从右向左来进行扫描的。

匹配相同的文本两次
如果在正则表达式的开始处使用逆序环视,或者在正则表达式的结尾处使用顺序环视,其效果就是要求在正则匹配之前或者之后出现一些东西,但不要把它们包含到匹配中。如果在正则表达式的中间使用环视的话,就可以对同一段文本进行多次测试。

在实例2.3的“流派相关的特性”小节中,我们讲解了如何使用字符组补集来匹配一个泰国语的数字。只有在.NET和Java中才会支持字符组补集。

如果一个字符既是泰国语字符(任何类别),又是数字(任意字母表),那么它就是一个泰国语数字。如果使用顺序环视,你可以在同一个字符上检查这两个要求:

(?=\p{Thai})\p{N}
正则选项:无
正则流派:PCRE、Perl、Ruby 1.9

这个正则表达式只能用于在实例2.7所讲解的支持Unicode字母表的3种流派。但是使用顺序环视来多次匹配同一个字符的思想则可以用于本书中讨论的所有流派。

当正则引擎查找‹(?=p{Thai})p{N}›的时候,它首先会在开始进行匹配尝试的字符串中的每一个位置进入顺序环视。如果该位置的字符不在泰国语字母表(也就是说‹p{Thai}›匹配失败)中,那么这次顺序环视就会失败。这也会导致整个匹配尝试失败,并迫使正则引擎到下一个字符处重新进行尝试。

如果正则表达式遇到一个泰国语字符,‹p{Thai}›成功匹配。因此,环视‹(?=p{Thai})›也会匹配成功。当引擎退出环视的时候,它会恢复之前的匹配进程。在这个例子中,也就是在刚找到泰国语字符之前的长度为0的匹配。接下来要匹配的是‹p{N}›。因为顺序环视已经丢弃了它的匹配,因此‹p{N}›会同‹p{Thai}›已经匹配了的那个字符进行比较。如果该字符拥有Unicode属性Number的话,那么‹p{N}›会匹配成功。因为‹p{N}›并不在环视之内,所以它会真正匹配这个字符,同时我们也就找到了想要的泰国语数字。

环视是固化分组
当正则表达式引擎退出一个环视分组的时候,它会丢弃掉环视匹配的文本。因为该文本被丢弃了,所以由位于环视之内的选择分支或者量词所记住的任意回溯位置也都会被丢弃。这样实际上就会把顺序环视和逆序环视都变成了固化分组。实例2.15中详细讲解了固化分组的概念。

在绝大多数情形下,环视的原子特性是无关紧要的。一个环视只是用来检查位于环视中的正则表达式是匹配成功还是失败的断言。它可以通过多少种不同方式匹配并不重要,因为它不会消耗目标文本中的任何字符。

当你在顺序环视(以及逆序环视,如果你的正则流派支持)之内使用捕获分组的时候,它的固化特性才会产生意义。虽然顺序环视不会消耗任何文本,但是正则引擎会记住文本中哪些部分被位于顺序环视中的任何捕获分组匹配了。如果顺序环视位于正则表达式的结尾处,那么实际上你的捕获分组所匹配的文本是正则表达式自身没有匹配的。如果这个顺序环视位于正则表达式中间,那么你的多个捕获分组匹配到的目标文本可能会相重叠。

环视的固化特性唯一能改变整个正则表达式的匹配的情况是,你在环视之外使用一个反向引用来指向在环视之内所创建的捕获分组。思考这个正则表达式:

(?=(\d+))\w+\1
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby

乍一看,你可能会认为这个正则表达式能够匹配123x12。‹d+›会把12捕获到第一个捕获分组中,接着‹w+›会匹配3x,最后‹1›会再次匹配12。

但这不可能发生。正则表达式会进入环视及捕获分组。贪心的‹d+›会匹配123。这个匹配存储到第一个捕获分组中。引擎接着退出顺序环视,把当前匹配重新设置为字符串的开始,并且丢弃由贪心的加号所记住的回溯位置,但是会在第一个捕获分组中保留所存储的123。

现在,贪心的‹w+›会在字符串开始处进行尝试。它会把123x12都吃掉。这时指向123的‹1›在字符串结尾处匹配失败。‹w+›会回溯一个字符。‹1›还是会失败。‹w+›会继续回溯,直到它放弃了除了目标文本中第一个1之外的所有字符。‹1›在第一个1之后还是会匹配失败。

如果正则引擎能够返回到顺序环视中,放弃123而选择12,那么最后的12会匹配‹1›。但是正则引擎并不会这样做。

正则引擎此时并不存在可以选择的任何回溯位置。‹w+›已经回退到头了,而环视迫使‹d+›把它的回溯位置都丢掉了。因此匹配尝试会宣告失败。

代替逆序环视

<b>\K\w+(?=</b>)
正则选项:不区分大小写
正则流派:PCRE 7.2、Perl 5.10

Perl 5.10、PCRE 7.2及更高版本,提供了使用‹K›代替逆序环视的机制。当正则引擎在正则表达式中遇到‹K›时,引擎会保持之前所匹配的文本。匹配尝试会如不存在‹K›一样继续。但‹K›之前所匹配的文本并不会包含在整个匹配结果中。‹K›之前的捕获分组所保存的文本仍可以用于‹K›之后的反向引用。只有整个匹配结果受‹K›影响。

结果是许多情况下都可以使用‹K›代替肯定型逆序环视。与‹(?<=before)text›相同,‹beforeKtext›仅匹配紧跟在before之后的text。在Perl和PCRE中使用‹K›而非肯定型逆序环视的好处是可以在‹K›前使用完整的正则表达式语法,而逆序环视有各种限制,如不允许使用量词。

‹K›和逆序环视的主要区别是,使用‹K›时,正则式严格地从左向右匹配。它永远不会向回看,而逆序环视则会向回看。当‹K›后面或逆向环视后面的匹配,与‹K›前面或逆向环视内匹配的文本相同时,这个区别就会带来影响。

正则式‹(?<=a)a›在字符串aaa中可以找到2个匹配。在字符串开始处进行的第一次匹配尝试会失败,因为正则引擎无法在开始处之前找到一个a。在第一和第二个a之间的位置进行的匹配尝试可以成功。正则引擎向回看找到字符串中第一个a满足逆序环视条件,正则式中第二个‹a›匹配字符串中第二个a。在第二和第三个a之间的位置进行的匹配尝试同样会成功。正则引擎向回看找到字符串中第二个a满足逆序环视条件,随后正则式匹配第三个a。在字符串末尾进行的最后一次匹配尝试失败。向回看第三个a满足逆序环视条件,但字符串中没有更多字符可以匹配正则式第二个‹a›。

正则式‹aKa›只能在同一字符串中找到1个匹配。在字符串开始处进行的第一次匹配尝试成功。正则式中第一个‹a›匹配字符串中第一个a。‹K›将这一部分匹配排除出最终返回的匹配结果,但并不改变当前匹配过程。随后正则式中第二个‹a›匹配字符串中第二个a,作为最终返回的完整匹配结果。第二次匹配尝试从字符串中第二和第三个a之间的位置开始。正则式中第一个‹a›匹配字符串中第三个a。‹K›将这一匹配排除出最终匹配结果,正则引擎正常前进。不过字符串中已经没有字符可供正则式中第二个‹a›匹配,于是匹配尝试失败。

由此可见,使用‹K›时,正则式匹配过程正常进行。正则式‹aKa›找到的匹配与正则式‹a(a)›中捕获分组的匹配相同。你不能用‹K›多次匹配字符串中同一部分。而逆序环视则可以。可以用‹(?<=p{Thai})(?<=p{Nd})a›匹配一个紧跟在属于泰语字母表和数字的单个字符之后的a。如果尝试的是‹p{Thai}Kp{Nd}Ka›,那匹配的则是一个泰语字符紧跟一个数字再紧跟一个a,即使只返回a作为匹配结果。而这和使用‹p{Thai}p{Nd}(a)›匹配相同3个字符时,捕获分组所返回的结果相一致。

不使用逆序环视的解决方案
虽然前面讲的这么复杂,但是如果你用的是Ruby 1.8或者JavaScript,那么这些对你都毫无用处,因为你根本就不能使用逆序环视。使用这两种正则流派无法以上述方式解决前面所给出的问题,但是你可以通过使用捕获分组来解决需要逆序环视的问题。下面给出的这个替代方案也可以在所有其他正则流派中使用:

(<b>)(\w+)(?=</b>)
正则选项:不区分大小写
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby

作为逆序环视的替代,我们使用了一个捕获分组来匹配起始标签:‹< b >›。我们还把所需要的匹配部分,也就是‹w+›,放到了另一个捕获分组中。

当把这个正则表达式应用到My < b >cat b > is furry之上的时候,这个正则表达式的完整匹配会是< b >cat。第一个捕获分组会保存< b >,而第二个会保存cat。

如果题目的要求是只匹配cat(在两个< b >标签之间的单词),即你只想提取文本中的这部分内容的话,那么可以通过只保存第二个捕获分组所匹配的文本,而不是整个正则表达式匹配的文本来达到这一目标。

如果要求是想要进行查找和替换,而只替换在两个标签之间的单词的话,那么可以使用一个反向引用来指向第一个捕获分组,把起始标签重新添加到替代文本中。在这个例子中,实际上并不需要捕获分组,因为起始标签总是相同的。但是当它可变的时候,捕获分组会重新插入与前面匹配到的一模一样的内容。实例2.21对此有更详细的讲解。

最后,如果你真的想要模拟逆序环视的话,可以使用两个正则表达式来完成。首先,使用普通表达式,而不是不使用逆序环视,来查找你的正则表达式。当它匹配成功时,把在匹配部分前面的目标文本子串复制到一个新的字符串变量中。然后用第二个正则表达式,加上字符串结束定位符(‹z›或‹$›),进行你在逆序环视中所做的测试。这个定位符会确保第二个正则式的匹配一定位于该字符串的结尾。因为剪切字符串的地方是第一个正则表达式匹配的地方,所以这样就会把第二个匹配刚好放到第一个匹配的左边。

在JavaScript中,可以使用如下的代码来完成这项任务:

var mainregexp = /\w+(?=<\/b>)/;
var lookbehind = /<b>$/;
if (match = mainregexp.exec("My <b>cat</b> is furry")) {
     // Found a word before a closing tag </b>
     var potentialmatch = match[0];
     var leftContext = match.input.substring(0, match.index);
     if (lookbehind.exec(leftContext)) {
          // Lookbehind matched:
          // potentialmatch occurs between a pair of <b> tags
     } else {
          // Lookbehind failed: potentialmatch is no good
     }
} else {
     // Unable to find a word before a closing tag </b>
}
相关文章
|
1月前
|
Java 程序员
Java 异常处理与正则表达式详解,实例演练及最佳实践
在 Java 代码执行期间,可能会发生各种错误,包括程序员编码错误、用户输入错误以及其他不可预料的状况。 当错误发生时,Java 通常会停止并生成错误消息,这个过程称为抛出异常。 try...catch 语句 try 语句允许您定义一段代码块,并在其中测试是否发生错误。 catch 语句允许您定义一段代码块,当 try 块中发生错误时执行该代码块。 try 和 catch 关键字成对使用,语法如下:
42 0
|
2月前
|
存储 弹性计算 运维
阿里云服务器ECS经济型e实例详细介绍_性能测试和租用价格
阿里云服务器ECS经济型e实例详细介绍_性能测试和租用价格,阿里云服务器ECS推出经济型e系列,经济型e实例是阿里云面向个人开发者、学生、小微企业,在中小型网站建设、开发测试、轻量级应用等场景推出的全新入门级云服务器,CPU采用Intel Xeon Platinum架构处理器,支持1:1、1:2、1:4多种处理器内存配比,e系列性价比优选
|
4月前
|
机器学习/深度学习 存储 JavaScript
正则表达式基础语法与Java、JS使用实例
正则表达式基础语法与Java、JS使用实例
72 1
|
7月前
|
Java
Java正则表达式校验实例
Java正则表达式校验实例
50 0
|
1月前
|
Java
java面向对象高级分层实例_测试类(main方法所在的类)
java面向对象高级分层实例_测试类(main方法所在的类)
10 1
|
1月前
|
测试技术 Android开发
快速上手App自动化测试利器,Toast原理解析及操作实例
`Toast`是Android中的轻量级通知,短暂显示在屏幕任意位置,1-2秒后自动消失,不获取焦点且不可点击。Appium通过uiautomator2在控件树中处理Toast。在测试中,可设置隐式等待,利用XPath或Accessibility ID定位Toast元素进行检测和验证。示例代码展示了如何初始化driver,点击触发Toast,以及如何定位并读取Toast文本。
24 3
|
2月前
|
开发者 Python
Python中的正则表达式:re模块详解与实例
Python中的正则表达式:re模块详解与实例
|
2月前
|
存储 弹性计算 运维
阿里云经济型e实例详细介绍_性能测试_使用限制说明
阿里云服务器ECS推出经济型e系列,经济型e实例是阿里云面向个人开发者、学生、小微企业,在中小型网站建设、开发测试、轻量级应用等场景推出的全新入门级云服务器,CPU采用Intel Xeon Platinum架构处理器
|
4月前
|
弹性计算 测试技术 开发者
我使用了阿里云的e实例进行了一系列性能测试
我使用了阿里云的e实例进行了一系列性能测试
61 0
|
6月前
|
Kubernetes jenkins 测试技术
【Kubernetes的DevOps自动化,Jenkins上的Pipeline实现自动化构建、测试、部署、发布以及Bookinginfo实例的部署灰度发布故障注入流量】
【Kubernetes的DevOps自动化,Jenkins上的Pipeline实现自动化构建、测试、部署、发布以及Bookinginfo实例的部署灰度发布故障注入流量】
125 1