正则表达式——前后查找

前后查找

1、前后查找

我们还是先来看一个例子:你要把一个Web页面的页面标题提取出来。HTML页面标题是出现在<TITLE>和</TITLE>标签之间的文字,而这对标签又必须嵌在HTML代码的<HEAD>部分里。下面就是这个例子:

文本:

js 复制代码
<HEAD>
<TITLE>Ben Fota's Homepage</TITLE>
</HEAD>

正则表达式:

js 复制代码
<[tT][iI][tT][lL][eE]>.*<\/[tT][iI][tT][lL][eE]>

结果:

<[tT]​[iI]​[tT]​[lL]​[eE]>.*</[tT]​[iI]​[tT]​[lL]​[eE]> 匹配
标签(大写、小写或大小写混用)、</TITLE>标签以及这两个标签之间的任何文字。这个模式的效果与我们的预期基本相符,但不够理想。

为什么这么说呢?因为只有页面标题才是我们需要的,而我们找到的匹配里还包含着<TITLE>和</TITLE>标签。能不能只返回页面标题的文字部分呢?

办法之一是使用子表达式。我们可以利用子表达式把被匹配文本划分为3个部分:开始标签、标题文字、结束标签。把被匹配文本划分为多个部分之后,从它们当中提取且只提取出我们需要的东西就很容易了。

可是,明知是自己并不真正需要的东西(比如上例中的<TITLE>和</TITLE>标签)​,还把它们检索出来岂不是毫无意义。​"先想办法把它们检索出来、再以手动方式排除它们"这既浪费时间,又容易招致不必要的后患。在遇到这类问题的时候,你真正需要的是这样一个模式,它包含的匹配本身并不返回,而是用于确定正确的匹配位置,它并不是匹配结果的一部分。换句话说,你需要进行"前后查找"。

本章将对向前查找(lookahead)和向后查找(lookbehind)都进行讨论。常见的正则表达式实现都支持前者,但支持后者的就没那么多了。Java、.NET、PHP和Perl都支持向后查找(但有一些限制), JavaScript和ColdFusion不支持向后查找。

2、向前查找

向前查找指定了一个必须匹配但不在结果中返回的模式。向前查找实际就是一个子表达式,而且从格式上看也确实如此。从语法上看,一个向前查找模式其实就是一个以?=开头的子表达式,需要匹配的文本跟在=的后面。

我们来看一个例子。例子里的原始文本是一些URL地址,而你的任务是把它们的协议名部分提取出来(为下一步处理做准备)​。下面就是这个例子:

文本:

js 复制代码
http://www.forta.com/
https://mail.forta.com/
ftp://ftp.forta.com/

正则表达式:

js 复制代码
.+(?=:)

结果:

在上面列出的URL地址里,协议名与主机名之间以一个:分隔。模式.+匹配任意文本(第1个匹配是http)​,子表达式(? =:)匹配:​。注意,被匹配到的:并没有出现在最终的匹配结果里;我们用?=向正则表达式引擎表明:只要找到:就行了,不要把它包括在最终的匹配结果里------用术语来说,就是"不消费"它。

为了更好地理解?=的作用,我们再来看一个同样的例子,但这次不使用向前查找元字符:

正则表达式:

js 复制代码
.+(:)

结果:

子表达式(:)正确地匹配到了:并消费了这个字符------:出现在了最终的匹配结果里。

这两个例子的区别是前一个用来匹配:的模式是(?=:)​,后一个用来匹配:的模式是(:)​。这两个模式所匹配的东西是一样的------都是紧跟在协议名后面的那个:​,它们之间的区别只是被匹配到的:字符有没有出现在最终的匹配结果里而已。在使用向前查找的时候,正则表达式分析器将向前查找并处理:匹配,但不会把它包括在最终的搜索结果里。模式.+(:)查找到并且匹配结果包含:​,模式.+(? =:)查找到但匹配结果不包含:​。

注意 向前查找(和向后查找)匹配本身其实是有返回结果的,只是这个结果的字节长度永远是0而已。因此,前后查找操作有时也被称为零宽度(zero-width)匹配操作。
任何一个子表达式都可以转换为一个向前查找表达式,只要给它加上一个?=前缀即可。在同一个搜索模式里可以使用多个向前查找表达式,它们可以出现在模式里的任意位置(而不仅仅是出现在整个模式的开头------就像你们在上面看到的那样)​。

3、向后查找

正如你刚看到的那样,?=将向前查找(查找出现在被匹配文本之后的字符,但不消费那个字符)​。因此,?=被称为向前查找操作符。除了向前查找,许多正则表达式实现还支持向后查找,也就是查找出现在被匹配文本之前的字符(但不消费它)​,向后查找操作符?<=

?<=?=的具体使用方法大同小异;它必须用在一个子表达式里,而且后跟要匹配的文本。

下面是一个例子。你从某个数据库里搜索出了一份产品目录,但你只需要把那些产品的价格提取出来:

文本:

js 复制代码
ABC01: $23.45
HGG42: $5.31
CFMX1: $899.00
XTC99: $69.96
Total items found: 4

正则表达式:

js 复制代码
\$[0-9.]+

结果:

\$匹配$, [0-9.]+匹配价格。

如上所示的匹配结果符合你的预期。但如果你不想让$出现在最终的匹配结果里,你该怎么办?从这个模式里简单地把$去掉能达到目的吗?

正则表达式:

js 复制代码
[0-9.]+

结果:

这显然不是你想要的结果。你需要\$来确定应该匹配哪些文本,你只是不想让$出现在最终的匹配结果里而已。

怎么办?好办,这正是向后查找可以大显身手的地方,如下所示:

正则表达式:

js 复制代码
(?<=\$)[0-9.]+

结果:

问题迎刃而解了。​(? <=\$)只匹配$,但不消费它;最终的匹配结果里只有价格数字(没有前缀的$字符)​。

警告 向前查找模式的长度是可变的,它们可以包含.和+之类的元字符,所以它们非常灵活。而向后查找模式只能是固定长度------这是一条几乎所有的正则表达式实现都遵守的限制。

4、把向前查找和向后查找结合起来

向前查找和向后查找可以组合在一起使用,就像下面这个例子所演示的那样(这个例子解决了我们在本章刚开始时提出的问题)​:

文本:

js 复制代码
<HEAD>
<TITLE>Ben Fota's Homepage</TITLE>
</HEAD>

正则表达式:

js 复制代码
(?<=<[tT][iI][tT][lL][eE]>).*(?=<\/[tT][iI][tT][lL][eE]>)

结果:

问题解决了。​(? <=<tTiItTlLeE>)是一个向后查找操作,它匹配(但不消费)<TITLE>;而(? =<\/tTiItTlLeE>)是一个向前查找操作,它匹配(但不消费)</TITLE>。最终返回的匹配结果包含且仅包含标题文字(用术语来说,就是只有标题文字被消费了)​。

5、对前后查找取非

到目前为止正如你看到的那样,向前查找和向后查找通常用来匹配文本,其目的是为了确定将被返回为匹配结果的文本的位置(通过指定匹配结果的前后必须是哪些文本)​。这种用法被称为正向前查找(positivelookahead)和正向后查找(positive lookbehind)​。术语"正"指的是寻找匹配的事实。

前后查找还有一种不太常见的用法叫做负前后查找(negative lookaround)。负向前查找(negativelookahead)将向前查找不与给定模式相匹配的文本,负向后查找(negative lookbehind)将向后查找不与给定模式相匹配的文本。

我们曾经介绍过一个用来对字符集合进行取非处理的操作符^,但^不能用来对前后查找进行取非处理。这里必须使用另外一种语法:前后查找必须用来取非(它将替换掉=)​。下表列出了所有的前后查找操作符。

一般来说,凡是支持向前查找的正则表达式实现都同时支持正向前查找和负向前查找。类似地,凡是支持向后查找的正则表达式实现都同时支持正向后查找和负向后查找。

为了演示正向后查找和负向后查找之间的区别,我们来看一个例子。下面是一段包含着一些数值的文本,其中既有价格又有数量。我们先来查找且只查找价格:

文本:

js 复制代码
I paid $30 for 100 apples,
50 oranges, and 60 pears.
I saved $5 on this order.

正则表达式:

js 复制代码
(?<=\$)\d+

结果:

这与我们刚见过的例子非常相似。\d+匹配数值(一个或多个数字字符),(? <=\$)向后查找(但不消费)字符$(这个字符在模式里被转义为$)​。这个模式正确地匹配到了两个用来表示价格的数值,那些用来表示数量的数字没有出现在最终的匹配结果里。

接下来,我们再去查找且只查找数量:

正则表达式:

js 复制代码
\b(?<!\$)\d+\b

结果:

\d+还是匹配数值,但这次只匹配数量,不匹配价格。表达式(? <! \$)是一个负向后查找,它使得最终的匹配结果只包含那些不以$开头的数值。把操作符?<=改为操作符?<!使得整个模式从一个正向后查找变成了一个负向后查找。

在上面这个例子里,我们还在那个负向后查找模式里用\b元字符定义了两个单词边界。我们为什么要那么做呢?你看过下面这个没有使用单词边界的例子里就明白了。

正则表达式:

js 复制代码
(?<!\$)\d+

结果:

请看,因为没有使用单词边界,30里的0也出现在了最终的匹配结果里。这是因为那个0字符的前一个字符是3而不是,它完全符合模式(? <! $)\d+的匹配要求。把这个模式用\b括起来从根本上解决了这个问题。

相关推荐
吴声子夜歌2 小时前
正则表达式参考
正则表达式
吴声子夜歌20 小时前
正则表达式——常见应用软件和编程语言中的正则表达式
正则表达式
吴声子夜歌1 天前
正则表达式——使用子表达式
正则表达式
吴声子夜歌2 天前
正则表达式——重复匹配
正则表达式
吴声子夜歌2 天前
正则表达式——匹配单个字符
正则表达式
吴声子夜歌2 天前
正则表达式——位置匹配
正则表达式
Dawn-bit5 天前
Linux日志处理三剑客之基础篇:(基础正则+扩展正则)
linux·运维·服务器·正则表达式·云计算·运维开发
寒水馨5 天前
macOS下载、安装ripgrep-15.2.0(附安装包ripgrep-15.2.0-aarch64-apple-darwin.tar.gz)
macos·正则表达式·rust·ripgrep·rg·命令行搜索·代码搜索
冰暮流星10 天前
javascript之正则表达式声明
正则表达式