在前面的章节中,你已经学习到如何通过元字符和特殊的类集合来匹配字符。在本章中,你将学习到如何匹配多个重复的字符或者字符集合。
有多少匹配?
你已经学习了正则表达式匹配的基础知识,但是已经展示的例子都有一个限制。考虑这样一个场景:写一个匹配电子邮件的正则表达式。电子邮件地址的格式如下所示:
text@text.text
使用前面章节中介绍过的元字符,你可以创建一个如下面的正则表达式:
\w@\w\.\w
“ \w ”将匹配所有的文字数字式字符(包括下划线,这在 Email 地址中是合法的);“ @ ”不需要转义,但是“ . ”需要。
这是一个合法的正则表达式,但是一般没有用。它可以匹配形如a@b.c(尽管是合法的,但是很明显不是一个可用的邮件地址)的邮件地址。现在的问题是“ \w ”只能够匹配一个字符,而你却不知道需要测试多少个字符。总之,下面的地址都是合法的邮件地址,但是在 @ 前面却又着不同数量的字符。
b@forta.com
ben@forta.com
bforta@forta.com
现在需要的方法是如何匹配多个字符,而这些是可以通过几个特殊元字符中的一种来实现的。
匹配一个或更多字符
需要匹配一个或者多个字符的时候,直接在后面加上“ + ”符号即可。“ + ”元字符匹配一个或者多个字符(至少有一个, 0 个将是不会匹配的)。所以,“ a ”将匹配“ a ”,而“ a+ ”则匹配一个或者多个“ a ”。同样的,“ [0-9] ”匹配任意的一个数字,“ [0-9]+ ”匹配一个或者多个数字。
提示:当在字符集合上使用“ + ”的时候,需要将“ + ”符号放在集合外面。因此,“ [0-9]+ ”是正确的,而“ [0-9+] ”是不正确的。当然,“ [0-9+] ”也是一个合法的正则表达式,但是却不是匹配一个或者多个数字,而是定义了一个包含 0 到 0 和 + 符号的字符集合,所以将匹配一个单个的数字或者“ + ”符号。所以,尽管这个正则表达式是合法的,但是可能不是你所需要的。
让我们重新来看看这个 Email 地址的例子,现在通过“ + ”来匹配一个或者多个字符:
文本
Send personal email to ben@forta.com. For questions
about a book use support@forta.com. Feel free to send
unsolicited email to spam@forta.com (wouldn't it be
nice if it were that simple, huh?).
正则表达式
\w+@\w+\.\w+
结果
Send personal email to ben@forta.com . For questions
about a book use support@forta.com . Feel free to send
unsolicited email to spam@forta.com (wouldn't it be
nice if it were that simple, huh?).
分析
这个模式正确匹配了所有的三个 Email 地址。正则表达式首先通过“ \w ”来匹配了一个或者多个文字数字式字符。然后在匹配 @ 之后同样通过“ \w ”匹配了一个或者多个文字数字式字符。接着是匹配“ . ”(通过“ \. ”),接着是另外一个文字数字式字符,从而完成了对 Email 地址的匹配。
提示:“ + ”是一个元字符,匹配“ + ”需要使用转义“ \+ ”。“ + ”符号同样可以用于一个或者多个字符集合。为了演示这点,下面的例子使用了相同的正则表达式和稍修改后的文本。
文本
Send personal email to ben@forta.com or
ben.forta@forta.com. For questions about a
book use support@forta.com. If your message
is urgent try ben@urgent.forta.com. Feel
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
正则表达式
\w+@\w+\.\w+
结果
Send personal email to ben@forta.com or
ben.forta@forta.com . For questions about a
book use support@forta.com . If your message
is urgent try ben@urgent.forta .com. Feel
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
分析
这里的正则表达式匹配了五个 Email 地址,但是其中两个是不完整的。为什么会这样?“\w+@\w+\.\w+”不会匹配 @ 之前的字符“ . ”,而在 @ 之后则只允许有一个“ . ”字符。所以,尽管“ ben.forta@forta.com ”是一个合法的 Email 地址,但是这里的正则表达式只会匹配forta而不是ben.forta,因为“ \w ”将匹配文字数字式字符而不会匹配文本中的“ . ”字符。
这里我们的需求是匹配“ \w ”或者“ . ”,用正则表达式的语言说就是一个集合“[\w\.]”。下面是个例子:
文本
Send personal email to ben@forta.com or
ben.forta@forta.com. For questions about a
book use support@forta.com. If your message
is urgent try ben@urgent.forta.com. Feel
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
正则表达式
[\w.]+@[\w.]+\.\w+
结果
Send personal email to ben@forta.com or
ben.forta@forta.com . For questions about a
book use support@forta.com . If your message
is urgent try ben@urgent.forta.com . Feel
free to send unsolicited email to
spam@forta.com (wouldn't it be nice if
it were that simple, huh?).
分析
看起来正则表达式已经可以工作了。 [\w.]+ 将匹配一个或者多个文字数字式字符、下划线和“ . ”符号,所以ben.forta可以匹配了。同样的,[\w.]+用在 @ 之后,可以匹配更深的域名(主机名)。
注意:注意到最后一个匹配使用的是“\w+”而不是“[\w.]+”。你知道为什么吗?试着使用“[\w.]”作为最后一个模式,看看在第二个、第三个、第四个匹配上如何发生错误的。
注意:你可能注意到这里集合中的“ . ”字符没有转义,而且匹配了“ . ”(在字符集合中“ . ”将作为字面字符而不是元字符)。一般的,元字符如“ . ”和“ + ”等用于字符集合的时候是作为字面含义使用的,因此没有必要转义。尽管如此,对其进行转义也没错,所以,[\w.]和[\w\.]的功能是一样的。
匹配零个或者更多字符
“ + ”符号匹配一个或者多个字符。零个字符是不会被匹配的——至少要有一个字符。但是如果希望作为一个可选字符,换句话说,需要零个字符匹配的时候如何做呢?
为了做到这点,可以使用“ * ”元字符。“ * ”字符和“ + ”字符的使用方法是一样的,也是在字符或者字符集合后加上“ * ”字符从而匹配零个或者多个字符和字符组。因此,模式“B.* Forta”可以匹配“B Forta”、“B. Forta”、“Ben Forta”或者其他组合。
为了演示“ + ”符号的用法,让我们来看看这个 Email 地址匹配的修改版本。
文本
Hello .ben@forta.com is my email address.
正则表达式
[\w.]+@[\w.]+\.\w+
结果
Hello .ben@forta.com is my email address.
分析
你可能会回忆起[\w.] + 将匹配一个或者多个文字数字式字符和“ . ”符号,所以“ . ”将匹配。实际上在前面的文本中有一个拼写错误(在文本中有个多余的“ . ”符号),但是这是不相关的。最大的问题在于尽管“ . ”符号在 Email 地址中是合法的,但是用在最前面却是不合法的。
换句话说,你真正需要的匹配是一个文字数字式字符后接一个可选的字符,就像下面这样:
文本
Hello .ben@forta.com is my email address.
正则表达式
\w+[\w.]*@[\w.]+\.\w+
结果
Hello .ben@forta.com is my email address.
分析
这个模式看起来越来越复杂(实际上并不是)。“\w+”将匹配文字数字式字符并不包含“ . ”( Email 地址的合法开始字符)。在这之后, Email 地址可以包含可选的文字数字式字符或者是“ . ”字符。“[\w.]*”将匹配零个或者多个“ . ”符号和文字数字式字符,这正是我们所需要的。
注意:想象“ * ”为了一个字符可选的元字符。不像“ + ”符号,至少要有一个匹配,“ * ”符号可以匹配任意数量,而且不是必须的。
“ * ”符号是元字符。为了能够匹配“ * ”,需要进行转义“ \* ”。
匹配零个或者一个字符
另外一个有用的元字符是“ ? ”。像“ + ”符号一样,“?”可以匹配可选的文本(零次匹配)。但是也不像“ + ”符号,“ ? ”匹配零个或者一个字符,而不超过一个。所以,“ ? ”非常适合于在文本中匹配一个可选的字符。
考虑下面的例子:
文本
The URL is http://www.forta.com/ , to connect
securely use https://www.forta.com/ instead.
正则表达式
http://[\w./]+
结果
The URL is http://www.forta.com/ , to connect
securely use https://www.forta.com/ instead.
分析
这个模式将匹配以 http://开头的 URL ,其后为[\w./]+,将匹配一个或者多个文字数字式字符、“ . ”或者斜线。这个模式只能匹配第一个 URL (以 http:// 开头的 ),而不你能匹配第二个 URL (以 https:// 开头的 )。这里使用“ s* ”(零个或者多个)并不能正常工作,因为这将匹配 httpsssss:// (这显然是非法的)。
解决方法是使用“ s? ”,看看下面的例子:
文本
The URL is http://www.forta.com/ , to connect
securely use https://www.forta.com/ instead.
正则表达式
https?://[\w./]+
结果
The URL is http://www.forta.com/ , to connect
securely use https://www.forta.com/ instead.
分析
这个模式中以“https?://”开始,“ ? ”符号意味着前面的字符( s )可以为零个或者一个字符。换句话说,“https?://”可以匹配 http:// 和 https:// (而不会是其他)。
顺便说一句,可以使用“ ? ”来解决前面章节中提到的一个问题。在那个例子中,使用了“ \r\n ”来匹配行结束,但是在 Unix 或者 Linux 下则需要使用 \n (没有 \r )来匹配,一个好的解决方法是使用一个可选的 \r 后接 \n 。下面是一个修改后的正则表达式:
文本
"101","Ben","Forta"
"102","Jim","James"
"103","Roberta","Robertson"
"104","Bob","Bobson"
正则表达式
[\r]?\n[\r]?\n
结果
"101","Ben","Forta"
"102","Jim","James"
"103","Roberta","Robertson"
"104","Bob","Bobson"
分析
“[\r]?\n”可以匹配一个可选的“ \r ”后接一个必须的“ \n ”。
提示:注意到这里的正则表达式使用了 “[\r] ? ”而不是“\r ? ”。“[\r]”定义了一个包含一个元素的字符集合。所以[\r] ? ”和“\r ? ”的作用是一样的。“[]”一般是用来定义多个元素的集合,但是有些开发者即使是在单个字符上也会加上用来避免歧义(知道需要匹配的特定字符)。如果同时使用“ [] ”和“ ? ”,则务必将“ ? ”放在集合外面。因此http[s]?:// 是正确的, 但是http[s?]:// 则不正确。
提示:“ ? ”符号是元字符。为了能够匹配“ ? ”,需要进行转义“ \? ”。
使用匹配次数
正则表达式中的+, *, 和?可以解决许多问题,但是有些时候并不够用。考虑下面的场景:
+ 和*提供了有限的匹配能力,但是却没有办法制定一个最大的匹配数量。
+, *, 和?元字符最小的匹配数量为 0 或者 1 。没有办法指定一个明确的最小匹配数量。
没有办法指定特定的匹配数量。
为了解决这个问题,能够提供对重复次数的精确控制,正则表达式允许指定匹配的次数。次数可以在“ { ”和“ } ”之间指定。
注意:“ { ”和“ } ”也是元字符,在使用字面含义的时候需要转义。值得注意的是,在很多的正则表达式实现中,不对“ { ”和“ } ”进行转义也可以正常处理(能够识别出是作为字面含义还是作为元字符)。尽管如此,最好不要依赖这种行为。当需要使用字面含义的时候,进行转义。
精确次数匹配
为了指定匹配的次数,你可以在“ { ”和“ } ”之间输入数字。因此,“ {3} ”将匹配 3 次前面出现的字符或集合。如果只有两个实例,则此模式不会匹配。
为了演示这点,来看看前面用过的 RGB 例子。回想一下, RGB 值是通过 3 组(每组两个字符)十六进制数字组成的。第一个匹配的模式如下:
#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]
在第四章中,使用了 POSIX 类并将模式改为如下:
#[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]
问题是这两个模式都必须要重复同样的组六次。下面是同样的例子,只是使用了匹配次数方法:
文本
<BODY BGCOLOR="#336633" TEXT="#FFFFFF"
MARGINWIDTH="0" MARGINHEIGHT="0"
TOPMARGIN="0" LEFTMARGIN="0">
正则表达式
#[[:xdigit:]]{6}
结果
<BODY BGCOLOR="#336633 " TEXT="#FFFFFF "
MARGINWIDTH="0" MARGINHEIGHT="0"
TOPMARGIN="0" LEFTMARGIN="0">
分析
[:xdigit:]匹配十六进制数字, {6} 则将匹配此 POSIX 类 6 次。这和使用#[0-9A-Fa-f]{6}模式是一样的。
次数区间匹配
次数还可以通过制定区间来匹配——使用最小值和最大值来确定匹配的数量。{ 2 , 4 }指示的区间为最小值 2 和最大值 4 。下面是使用正则表达式来验证日期的一个例子:
文本
4/8/03
10-6-2004
2/2/2
01-01-01
正则表达式
\d{1,2}[-\/]\d{1,2}[-\/]\d{2,4}
结果
4/8/03
10-6-2004
2/2/2
01-01-01
分析
这里列出的日期可能是用户在表单中输入的——这些值必须经过验证,确保是合乎格式的日期。“\d{1,2}”匹配一个或者两个数字(这个可以用于天数和月份);“\d{2,4}”用来匹配年;“[-\/]”则匹配“–”或者“/”(日期的分隔符)。就这样,匹配了三个日期。但是“ 2/2/2 ”没有被匹配(年太短了)。
提示:这里的正则表达式对“ / ”使用了转义“ \/ ”。在许多的正则表达式实现中,这并不是必须的,但是有些正则表达式分析器需要这样写。总是转义“ / ”是一个好的习惯。
需要提及的是这里的模式并没有对日期的合法性进行验证。如“54/67/9999”这样的日期也可以通过测试。这个模式所做的工作只是在验证各个部分之前对格式进行验证。
注意:匹配次数有可能开始于 0 。所以, {0,3} 将匹配零个、一个、两个和三个实例。在前面看到过, ? 元字符将匹配零个或者一个实例,所以 ? 和 {0,1} 的功能是一样的。
“至少”次数匹配
次数匹配的最后一个用法是可以指定匹配的最小值(没有最大值)。这种类型的语法和次数区间相似,但是省略了最大值。例如, {3,} 意味着最小要匹配 3 次,或者换句话说,匹配三次或者更多次。
让我们来看一个例子。在这个例子中,正则表达式用来定位值在 $100 或者更多的订单:
文本
1001: $496.80
1002: $1290.69
1003: $26.43
1004: $613.42
1005: $7.61
1006: $414.90
1007: $25.00
正则表达式
\d+: \$\d{3,}\.\d{2}
结果
1001: $496.80
1002: $1290.69
1003: $26.43
1004: $613.42
1005: $7.61
1006: $414.90
1007: $25.00
分析
前面的文本中是一个报告,包含订单号和订单值。正则表达式首先使用“\d+:”来匹配订单号(这个其实可以省略,因为在这种情况下匹配的价格不会包含整行)。模式“\$\d{3,}\.\d{2}”用来匹配价格。“\$” 匹配“$”,\d{3,}匹配至少三个数字(因此至少是 $100 ),“\.”匹配 “.”,最后“\d{2}”匹配两个数字。这个模式正确匹配了七个订单中的四个。
提示:在使用这种匹配次数的时候需要特别注意,如果忽略了“ , ”的话,这里的测试就从最小数量的匹配变为了精确次数的匹配。
笔记:“ + ”和 {1,}的作用是相同的。
非贪婪匹配
“ ? ”匹配是有限的(零个或者一个匹配),同样的当次数匹配精确的数量或者区间的时候也是一样的。但是在本章中介绍的其它类型的重复匹配可以匹配任意多的数量——有时候太多了。
到现在为止我们选择的例子都没有出现过度匹配的例子,但是看看下面的例子。下面的文本是一个包含了内置 HTML <B> 标签的 Web 页面。正则表达式需要匹配位于 <B> 标签中的文本(可能使用替换来进行格式化):
文本
This offer is not available to customers
living in <B>AK</B> and <B>HI</B>.
正则表达式
<[Bb]>.*</[Bb]>
结果
This offer is not available to customers
living in <B>AK</B> and <B>HI</B> .
分析
“<[Bb]>”匹配开始的“ <B> ”标签(包括大写和小写),“< / [Bb]>”匹配结束的“ </B> ”标签。但是并不是预想中的两个匹配,而只找到一个匹配。“ .* ”将匹配第一个 <B> 和最后一个 </B> 之间的所有文本,包括“AK</B>”和“<B>HI”。这包含了我们希望匹配的内容,但是也包含了标签的其他实例。
产生这个现象的原因是因为现在的元字符如“ * ”和“ + ”都是贪婪匹配。也就是说,正则表达式总是寻找最大的匹配,而不是最小的。也就是说匹配总是从最后开始匹配,回溯直到找到下一个匹配,而不是从头开始。这是故意设计的,数量词是贪婪的。
但是如果你不希望贪婪匹配的时候怎么办?解决办法是使用这些量词的非贪婪匹配(之所以说非贪婪是因为这个匹配总是匹配尽可能少的字符)。非贪婪量词是在量词后面加上“ ? ”。每个贪婪量词都有一个对应的非贪婪量词,见表 5.1 。
贪婪量词 | 非贪婪量词 |
* | *? |
+ | +? |
{n,} | {n,}? |
表5.1. 贪婪和非贪婪量词
“*?”是“ * ”的非贪婪版本,所以可以使用“ *? ”来修改上面的例子:
文本
This offer is not available to customers
living in <B>AK</B> and <B>HI</B>.
正则表达式
<[Bb]>.*?</[Bb]>
结果
This offer is not available to customers
living in <B>AK</B> and <B>HI</B> .
分析
使用非贪婪量词“ *? ”可以工作。 AK 和 HI 可以分别匹配。
注意:本书中的大量例子都是使用的贪婪量词,用来保持模式的简单性。尽管如此,在必要的时候可以更改成非贪婪量词。
小结
只有在使用重复匹配后正则表达式的威力才能体现出来。本章中介绍了“ + ”(匹配一个或者多个)、“ * ”(匹配零个或者多个)和“ ? ”(匹配零个或者一个)用来执行重复匹配。为了得到更大的控制,可以使用匹配次数来指定特定的数字或者是最大值和最小值。量词是贪婪的,有可能会造成过量匹配。为了阻止这种情况的发生,可以使用非贪婪量词。
@ 亦歌亦行 http://searun.iteye.com