[跟我学正则表达式] 5. 重复匹配

2009年5月3日星期日

[跟我学正则表达式] 5. 重复匹配


在前面的章节中,你已经学习到如何通过元字符和特殊的类集合来匹配字符。在本章中,你将学习到如何匹配多个重复的字符或者字符集合。

有多少匹配?
你已经学习了正则表达式匹配的基础知识,但是已经展示的例子都有一个限制。考虑这样一个场景:写一个匹配电子邮件的正则表达式。电子邮件地址的格式如下所示:
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地址的例子,现在通过“+”来匹配一个或者多个字符:
文本
Sendpersonalemailtoben@forta.com.Forquestions
aboutabookusesupport@forta.com.Feelfreetosend
unsolicitedemailtospam@forta.com(wouldn'titbe
niceifitwerethatsimple,huh?).
正则表达式
\w+@\w+\.\w+

结果
Sendpersonalemailtoben@forta.com.Forquestions
aboutabookusesupport@forta.com.Feelfreetosend
unsolicitedemailtospam@forta.com(wouldn'titbe
niceifitwerethatsimple,huh?).

分析
这个模式正确匹配了所有的三个Email地址。正则表达式首先通过“\w”来匹配了一个或者多个文字数字式字符。然后在匹配@之后同样通过“\w”匹配了一个或者多个文字数字式字符。接着是匹配“.”(通过“\.”),接着是另外一个文字数字式字符,从而完成了对Email地址的匹配。
提示:“+”是一个元字符,匹配“+”需要使用转义“\+”。“+”符号同样可以用于一个或者多个字符集合。为了演示这点,下面的例子使用了相同的正则表达式和稍修改后的文本。
文本
Sendpersonalemailtoben@forta.comor
ben.forta@forta.com.Forquestionsabouta
bookusesupport@forta.com.Ifyourmessage
isurgenttryben@urgent.forta.com.Feel
freetosendunsolicitedemailto
spam@forta.com(wouldn'titbeniceif
itwerethatsimple,huh?).

正则表达式
\w+@\w+\.\w+

结果
Sendpersonalemailtoben@forta.comor
ben.forta@forta.com.Forquestionsabouta
bookusesupport@forta.com.Ifyourmessage
isurgenttryben@urgent.forta.com.Feel
freetosendunsolicitedemailto
spam@forta.com(wouldn'titbeniceif
itwerethatsimple,huh?).

分析
这里的正则表达式匹配了五个Email地址,但是其中两个是不完整的。为什么会这样?“\w+@\w+\.\w+”不会匹配@之前的字符“.”,而在@之后则只允许有一个“.”字符。所以,尽管“ben.forta@forta.com”是一个合法的Email地址,但是这里的正则表达式只会匹配forta而不是ben.forta,因为“\w”将匹配文字数字式字符而不会匹配文本中的“.”字符。
这里我们的需求是匹配“\w”或者“.”,用正则表达式的语言说就是一个集合“[\w\.]”。下面是个例子:
文本
Sendpersonalemailtoben@forta.comor
ben.forta@forta.com.Forquestionsabouta
bookusesupport@forta.com.Ifyourmessage
isurgenttryben@urgent.forta.com.Feel
freetosendunsolicitedemailto
spam@forta.com(wouldn'titbeniceif
itwerethatsimple,huh?).

正则表达式
[\w.]+@[\w.]+\.\w+

结果
Sendpersonalemailtoben@forta.comor
ben.forta@forta.com.Forquestionsabouta
bookusesupport@forta.com.Ifyourmessage
isurgenttryben@urgent.forta.com.Feel
freetosendunsolicitedemailto
spam@forta.com(wouldn'titbeniceif
itwerethatsimple,huh?).

分析
看起来正则表达式已经可以工作了。[\w.]+将匹配一个或者多个文字数字式字符、下划线和“.”符号,所以ben.forta可以匹配了。同样的,[\w.]+用在@之后,可以匹配更深的域名(主机名)。
注意:注意到最后一个匹配使用的是“\w+”而不是“[\w.]+”。你知道为什么吗?试着使用“[\w.]”作为最后一个模式,看看在第二个、第三个、第四个匹配上如何发生错误的。
注意:你可能注意到这里集合中的“.”字符没有转义,而且匹配了“.”(在字符集合中“.”将作为字面字符而不是元字符)。一般的,元字符如“.”和“+”等用于字符集合的时候是作为字面含义使用的,因此没有必要转义。尽管如此,对其进行转义也没错,所以,[\w.]和[\w\.]的功能是一样的。

匹配零个或者更多字符
“+”符号匹配一个或者多个字符。零个字符是不会被匹配的——至少要有一个字符。但是如果希望作为一个可选字符,换句话说,需要零个字符匹配的时候如何做呢?
为了做到这点,可以使用“*”元字符。“*”字符和“+”字符的使用方法是一样的,也是在字符或者字符集合后加上“*”字符从而匹配零个或者多个字符和字符组。因此,模式“B.*Forta”可以匹配“BForta”、“B.Forta”、“BenForta”或者其他组合。
为了演示“+”符号的用法,让我们来看看这个Email地址匹配的修改版本。
文本
Hello.ben@forta.comismyemailaddress.
正则表达式
[\w.]+@[\w.]+\.\w+

结果
Hello.ben@forta.comismyemailaddress.

分析
你可能会回忆起[\w.]+将匹配一个或者多个文字数字式字符和“.”符号,所以“.”将匹配。实际上在前面的文本中有一个拼写错误(在文本中有个多余的“.”符号),但是这是不相关的。最大的问题在于尽管“.”符号在Email地址中是合法的,但是用在最前面却是不合法的。
换句话说,你真正需要的匹配是一个文字数字式字符后接一个可选的字符,就像下面这样:
文本
Hello.ben@forta.comismyemailaddress.

正则表达式
\w+[\w.]*@[\w.]+\.\w+

结果
Hello.ben@forta.comismyemailaddress.

分析
这个模式看起来越来越复杂(实际上并不是)。“\w+”将匹配文字数字式字符并不包含“.”(Email地址的合法开始字符)。在这之后,Email地址可以包含可选的文字数字式字符或者是“.”字符。“[\w.]*”将匹配零个或者多个“.”符号和文字数字式字符,这正是我们所需要的。

注意:想象“*”为了一个字符可选的元字符。不像“+”符号,至少要有一个匹配,“*”符号可以匹配任意数量,而且不是必须的。
“*”符号是元字符。为了能够匹配“*”,需要进行转义“\*”。

匹配零个或者一个字符
另外一个有用的元字符是“?”。像“+”符号一样,“?”可以匹配可选的文本(零次匹配)。但是也不像“+”符号,“?”匹配零个或者一个字符,而不超过一个。所以,“?”非常适合于在文本中匹配一个可选的字符。
考虑下面的例子:
文本
TheURLis,toconnect
securelyuseinstead.

正则表达式


结果
TheURLis,toconnect
securelyuseinstead.

分析
这个模式将匹配以http://开头的URL,其后为[\w./]+,将匹配一个或者多个文字数字式字符、“.”或者斜线。这个模式只能匹配第一个URL(以http://开头的),而不你能匹配第二个URL(以https://开头的)。这里使用“s*”(零个或者多个)并不能正常工作,因为这将匹配httpsssss://(这显然是非法的)。
解决方法是使用“s?”,看看下面的例子:
文本
TheURLis,toconnect
securelyuseinstead.

正则表达式
https?://[\w./]+

结果
TheURLis,toconnect
securelyuseinstead.

分析
这个模式中以“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:]]
问题是这两个模式都必须要重复同样的组六次。下面是同样的例子,只是使用了匹配次数方法:
文本
BODYBGCOLOR=""TEXT=""
MARGINWIDTH="0"MARGINHEIGHT="0"
TOPMARGIN="0"LEFTMARGIN="0"

正则表达式
#[[:xdigit:]]{6}

结果
BODYBGCOLOR=""TEXT=""
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,}的作用是相同的。

非贪婪匹配
“?”匹配是有限的(零个或者一个匹配),同样的当次数匹配精确的数量或者区间的时候也是一样的。但是在本章中介绍的其它类型的重复匹配可以匹配任意多的数量——有时候太多了。
到现在为止我们选择的例子都没有出现过度匹配的例子,但是看看下面的例子。下面的文本是一个包含了内置HTMLB标签的Web页面。正则表达式需要匹配位于B标签中的文本(可能使用替换来进行格式化):
文本
Thisofferisnotavailabletocustomers
livingiNBAK/BandBHI/B.
正则表达式
[Bb].*/[Bb]

结果
Thisofferisnotavailabletocustomers
livingiNBAK/BandBHI/B.
分析
“[Bb]”匹配开始的“B”标签(包括大写和小写),“/[Bb]”匹配结束的“/B”标签。但是并不是预想中的两个匹配,而只找到一个匹配。“.*”将匹配第一个B和最后一个/B之间的所有文本,包括“AK/B”和“BHI”。这包含了我们希望匹配的内容,但是也包含了标签的其他实例。

产生这个现象的原因是因为现在的元字符如“*”和“+”都是贪婪匹配。也就是说,正则表达式总是寻找最大的匹配,而不是最小的。也就是说匹配总是从最后开始匹配,回溯直到找到下一个匹配,而不是从头开始。这是故意设计的,数量词是贪婪的。
但是如果你不希望贪婪匹配的时候怎么办?解决办法是使用这些量词的非贪婪匹配(之所以说非贪婪是因为这个匹配总是匹配尽可能少的字符)。非贪婪量词是在量词后面加上“?”。每个贪婪量词都有一个对应的非贪婪量词,见表5.1。
贪婪量词
非贪婪量词
*
*?
+
+?
{n,}
{n,}?
表5.1.贪婪和非贪婪量词
“*?”是“*”的非贪婪版本,所以可以使用“*?”来修改上面的例子:
文本
Thisofferisnotavailabletocustomers
livingiNBAK/BandBHI/B.

正则表达式
[Bb].*?/[Bb]
结果
Thisofferisnotavailabletocustomers
livingiNBAK/BandBHI/B.

分析
使用非贪婪量词“*?”可以工作。AK和HI可以分别匹配。

注意:本书中的大量例子都是使用的贪婪量词,用来保持模式的简单性。尽管如此,在必要的时候可以更改成非贪婪量词。

小结
只有在使用重复匹配后正则表达式的威力才能体现出来。本章中介绍了“+”(匹配一个或者多个)、“*”(匹配零个或者多个)和“?”(匹配零个或者一个)用来执行重复匹配。为了得到更大的控制,可以使用匹配次数来指定特定的数字或者是最大值和最小值。量词是贪婪的,有可能会造成过量匹配。为了阻止这种情况的发生,可以使用非贪婪量词。

0 评论:

发表评论