回溯引用:前后一致匹配
1、回溯引用有什么用
为了理解回溯引用的概念,我们最好是看一个例子。HTML程序员经常使用标题标签(<H1>到<H6>,以及配对的结束标签)来定义和排版Web页面里的标题文字。现在,我们不妨假设你需要把某个Web页面里的所有标题文字全都查找出来,而不管它的级别是多少。下面就是这个例子:
文本:
<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
</BODY>
正则表达式:
<[hH]1>.*<\/[hH]1>

模式<[hH]1>.*<\/[hH]1>只能匹配一级标题(从<H1>或<h1>到</H1>或</h1>; HTML语言不区分字母的大小写)。但我们刚才说的是匹配任意级别的标题(HTML文档里的标题总共有6个级别),这应该怎么办呢?
最容易想到的办法是用一个字符集合来代替1,如下所示:
正则表达式:
<[hH][1-6]>.*?<\/[hH][1-6]>
结果:

这个模式看来不错,<[hH][1-6]>匹配任何一级标题的开始标签(具体到这个例子,它匹配到了<H1>和<H2>),</[hH][1-6]>匹配任何一级标题的结束标签(具体到这个例子,它匹配到了</H1>和</H2>)。
这里使用的是.?(懒惰型)而不是.(贪婪型)。我们在第5章里讲过,和其他几个元字符是“贪婪型”元字符,所以模式<[hH][1-6]>.</[hH][1-6]>有可能会从第2行的<H1>一直匹配到第6行的</H2>,这可不是我们想要的结果;使用“懒惰型”元字符.*?解决了这个问题。
之所以说“有可能”而不是“肯定”,是因为在这个特定的例子里即便是使用了“贪婪型”元字符也不一定会有问题。一般来说,元字符.不匹配换行符,而上例中的每个标题都各自占据一行。但在这里使用懒惰型元字符没有任何坏处——事前小心总比事后后悔好。
现在成功了吗?未必。看看下面这个例子(这次使用的是还是刚才那个模式),你就知道我为什么这样说了:
文本:
<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>
正则表达式:
<[hH][1-6]>.*?<\/[hH][1-6]>
结果:

在这个例子里,原始文本里有一个标题是以<H2>开头、以<H3>结束的。这显然是一个不合法的标题,但它与我们所使用的模式匹配上了。
出现这种情况的根源是这个模式的第2部分(用来匹配结束标签的那个部分)对这个模式的第1部分(用来匹配开始标签的那个部分)毫无所知。要想彻底解决这个问题,就只能求助于回溯引用。
2、回溯引用匹配
我们等会儿再去解决匹配HTML标题的问题。先来看一个比较简单的例子,这个问题如果不使用回溯引用将根本无法解决。
假设你有一段文本,你想把这段文本里所有连续重复出现的单词(打字错误,其中有一个单词输了两遍)找出来。显然,在搜索某个单词的第二次出现时,这个单词必须是已知的。回溯引用允许正则表达式模式引用前面的匹配结果(具体到这个例子,就是前面匹配到的单词)。
把这个问题弄明白的最佳办法是看看它到底是如何工作的。下面是一段包含着3组重复单词的文本,它们就是我们要找的东西:
文本:
This is a block of of text,
several words here are are
repeated, and and they
should not be.
正则表达式:
[ ]+(\w+)[ ]+\1
结果:

这个模式找到了我们想要的东西,但它是如何做到这一点的呢?[ ]+匹配一个或多个空格,\w+匹配一个或多个字母数字字符,[ ]+匹配随后的空格。注意,\w+是括在括号里的,它是一个子表达式。这个子表达式不是用来进行重复匹配的,这里根本不涉及重复匹配的问题。这个子表达式只是把整个模式的一部分单独划分出来以便在后面引用。这个模式的最后一部分是\1;这是一个回溯引用,而它引用的正是前面划分出来的那个子表达式:当(\w+)匹配到单词of的时候,\1也匹配单词of;当(\w+)匹配到单词and的时候,\1也匹配单词and。
回溯引用指的是模式的后半部分引用在前半部分中定义的子表达式(如上例所示)。
\1到底代表着什么?它代表着模式里的第1个子表达式,\2代表着第2个子表达式、\3代表着第3个;依次类推。于是,在上面那个例子里,[ ]+(\w+)[ ]+\1将匹配同一个单词的连续两次重复出现。
看过回溯引用的用法之后,我们再回过头来看看应该如何解决匹配HTML标题的问题。利用回溯引用,构造一个模式去匹配任何一级标题的开始标签和与之配对的结束标签(忽略任何不配对的标签组合)对我们来说已经不是什么难题了。下面就是这个例子:
文本:
<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>
正则表达式:
<[hH]([1-6])>.*?<\/[hH]\1>
结果:

总共找到了3个匹配:1个一级标题(<H1>…</H1>)和2个二级标题(<H2>…</H2>)。<[hH]([1-6])>匹配任何一级标题的开始标签,但我们这次用(和)把[1-6]括了起来,使它成为了一个子表达式。这样一来,我们就可以在用来匹配标题结束标签的</[hH]\1>用\1来引用这个子表达式了。子表达式([1-6])匹配数字1~6, \1只匹配与之相同的数字。这样一来,原始文本里的<H2>This isnot valid HTML</H3>就不会被匹配到了。
警告 回溯引用只能用来引用模式里的子表达式(用(和)括起来的正则表达式片段)。
提示 回溯引用匹配通常从1开始计数(\1、\2,等等)。在许多实现里,第0个匹配(\0)可以用来代表整个正则表达式。
注意 正如看到的那样,子表达式是通过它们的相对位置来引用的:\1对应着第1个子表达式,\5对应着第5个子表达式,等等。虽然受到普遍的支持,但这种语法存在着一个严重的不足:如果子表达式的相对位置发生了变化,整个模式也许就不能再完成原来的工作,删除或添加子表达式的后果可能更为严重。为了弥补这一不足,一些比较新的正则表达式实现还支持“命名捕获”(named capture):给某个子表达式起一个唯一的名字,然后用这个名字(而不是相对位置)来引用这个子表达式。因为命名捕获还没有得到广泛支持,而且已支持的实现具体的语法也极不统一,所以本书没有对此进行讨论。但是,如果你正在使用的正则表达式实现支持命名捕获功能(如.NET),你应该充分利用。
3、回溯引用在替换操作中的应用
到目前为止,你们在这本书里见到的正则表达式都是用来执行搜索的,即在一段文本里查找特定的内容。你在今后的实际工作中也会发现,你所编写的绝大多数正则表达式模式也可以用来搜索文本。但这并不是正则表达式的全部功能;正则表达式还可以用来完成各种复杂的替换操作。
简单的文本替换操作无须使用正则表达式就可以完成。比如说,如果只是把某个文档里的CA全部替换为California或把MI全部替换为Michigan的话,用正则表达式来完成这些替换就未免有点儿大材小用了。这句话的意思并不是说正则表达式不能用来执行这种替换,只是那么做没有什么实际价值。事实上,用普通的字符串处理函数来完成这种替换反而会更容易一些。
正则表达式更适用于复杂的替换,尤其是需要使用回溯引用的场合,那才能体现出正则表达式的真正威力。
文本:
Hello, ben@forta.com is my email address.
正则表达式:
\w+[\w\.]*@[\w\.]+\.\w+
结果:

这个模式可以把原始文本里的电子邮件地址查找出来。
现在,假设你需要把原始文本里的电子邮件地址全都转换为可点击的链接,你该怎么办?在HTML文档里,你需要使用<AHREF=“maito:user@address.com”>user@address.com这样的语法来创建一个可点击的电子邮件地址。能不能用一个正则表达式把一个电子邮件地址转换为这种可点击的地址格式呢?当然能,而且非常容易——但前提是你得使用回溯引用,如下所示:
文本:
Hello, ben@forta.com is my email address.
正则表达式:
(\w+[\w\.]*@[\w\.]+\.\w+)
替换:
<A HREF="mailto:$1">$1</A>
结果:
Hello, <A HREF="mailto:ben@forta.com">ben@forta.com</A> is my email address.
替换操作需要用到两个正则表达式:一个用来给出搜索模式,另一个用来给出匹配文本的替换模式。回溯引用可以跨模式使用,在第一个模式里被匹配的子表达式可以用在第二个模式里。这里使用的模式(\w+[\w\ .]*@[\w\.]+\ .\w+)与我们以前使用的完全一样(匹配电子邮件地址),但这次把它写成了一个子表达式。这样一来,被匹配到的文本就可以用在替换模式里了。<AHREF="mailto:$1">$1</A>使用了两次被匹配的子表达式:一次是在HREF属性里(来定义mailto:…),另一次是做为可点击文本。具体到这个例子,ben@forta.com变 成了<A HREF="mailto:ben@forta.com">ben@forta.com </A>,而这正是我们想要的结果。
警告 我们刚才讲过,回溯引用语法在不同的正则表达式实现里有很大的差异:JavaScript用户需要用$来代替; ColdFusion用户在查找和替换操作里都必须使用\。
提示 正如你在上面这个例子里看到的那样,同一个子表达式可以被引用任意多次——只要在需要用到它的地方写出它的回溯引用就行了。
我们再来看一个例子。在一个用来保存用户信息的数据库里,电话号码被保存为313-555-1234。现在,你需要把电话号码重新排版为(313)555-1234。下面就是这个例子:
文本:
313-555-1234
248-555-9999
810-555-9000
正则表达式:
(\d{3})(-)(\d{3})(-)(\d{4})

替换:
($1) $3-$5
结果:
(313) 555-1234
(248) 555-9999
(810) 555-9000
和刚才一样,这里也使用了两个正则表达式模式。第1个模式看起来很复杂,我们来分析一下。(\d{3})(-)(\d{3})(-)(\d{4})用来匹配一个电话号码,它被划分为5个子表达式(5个组成部分):第1个子表达式(\d{3})匹配前3位数字,第2个子表达式(-)匹配-字符,等等。最终的结果是一个电话号码被划分成了5个部分(每个部分分别对应着一个子表达式):区号、一个连字符、电话号码的前3位数字、又一个连字符、电话号码的后4位数字。这5个部分都可以单独拿出来使用,负责重新排版电话号码的替换模式($1)$3-$5只用到了它们当中的3个,剩下的两个没有用到,但这已足以把313-555-1234转换为(313)555-1234。
3.1、大小写转换
有些正则表达式实现允许我们使用表列出的元字符对字母进行大小写转换。

\l和\u只能把下一个字符(或子表达式)转换为小写或大写。\L和\U将把它后面的所有字符转换为小写或大写,直到遇上\E为止。
下面是一个简单的例子,把一级标题(<H1>…</H1>)的标题文字转换为大写:
文本:
<BODY>
<H1>Welcome to my Homepage</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>
正则表达式:
(<[Hh]1>)(.*?)(<\/[Hh]1>)

替换:
$1\U$2\E$3
结果:
<BODY>
<H1>WELCOME TO MY HOMEPAGE</H1>
Content is divided into two sections:<BR>
<H2>ColdFusion</H2>
Information about Macromedia ColdFusion.
<H2>Wireless</H2>
Information about Bluetooth, 802.11, and more.
<H2>This is not valid HTML</H3>
</BODY>
模式(<[Hh]1>)(.*?)(</[Hh]1>)把一级标题分成了3个子表达式:开始标签、标题文字、结束标签。第2个模式再把文本重新组合起来:$1包含着开始标签,\U$2\E把第2个子表达式(标题文字)转换为大写,$3包含着结束标签。

2454

被折叠的 条评论
为什么被折叠?



