ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

正则表达式——重复匹配

正则表达式——重复匹配

重复匹配

    • 1、有多少个匹配
      • 1.1、匹配一个或多个字符
      • 1.2、匹配零个或多个字符
      • 1.3、匹配零个或一个字符
    • 2、匹配的重复次数
      • 2.1、为重复匹配次数设定一个精确的值
      • 2.2、为重复匹配次数设定一个区间
      • 2.3、匹配“至少重复多少次”
    • 3、防止过度匹配

1、有多少个匹配

通过前面的学习,我们已经把正则表达式模式匹配操作的基础知识全都介绍给了大家,但我们给出的每个例子都有一个非常严格的限制。现在,请大家思考一下,如何构造一个匹配电子邮件地址的正则表达式。电子邮件地址的基本格式应该是如下所示的样子:

text@text.text

利用前一章讨论的元字符,你可能会写出一个如下所示的正则表达式:

\w@\w\.\w

\w可以匹配所有的字母和数字字符(以及下划线字符_,这个字符在电子邮件地址里是合法的)​;@字符不需要被转义,但.字符需要。

这个正则表达式本身没有任何错误,可它几乎没有任何实际的用处——它只能匹配a@b.c形式的电子邮件地址(虽然在语法方面没有任何问题,但这显然不是一个合法的地址)​。导致这一结果的关键是\w只能匹配单个字符,而我们无法预知电子邮件地址的各个字段会有多少个字符。举个最简单的例子,下面这些都是合法的电子邮件地址,但它们在@前面的字符个数都不一样。

b@forta.com ben@forta.com bforta@forta.com

要想解决这类问题,我们需要一种能够匹配多个字符的办法,这可以通过使用几种特殊的元字符来做到。

1.1、匹配一个或多个字符

要想匹配同一个字符(或字符集合)的多次重复,只要简单地给这个字符(或字符集合)加上一个+字符作为后缀就行了。+匹配一个或多个字符(至少一个;不匹配零个字符的情况)​。比如,a匹配a本身,a+将匹配一个或多个连续出现的a。类似地,​[0-9]匹配任意单个数字,​[0-9]+将匹配一个或多个连续的数字。

在给一个字符集合加上+后缀的时候,必须把+放在这个字符集合的外面。比如说,​[0-9]+是正确的,​[0-9+]则不是。​[0-9+]其实也是一个合法的正则表达式,但它匹配的不是一个或多个数字;它定义了一个由数字0到9和+构成的字符集合,因而只能匹配一个单个的数字字符或加号。虽然合法,可它并不是我们需要的东西。

重新回到电子邮件地址的例子,我们这次将使用+来匹配一个或多个字符:

文本:

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+

结果:

这个模式把原始文本里的3个电子邮件地址全都正确地匹配出来了。这个正则表达式先用第一个\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+

结果:


这个正则表达式匹配到了5个电子邮件地址,但其中有两个不够完整。为什么会这样?因为我们在构造这个正则表达式的时候只想到在@字符的后面会有一个.字符分开两个字符串的情况,没有想到在@字符的前面还会有.字符。因此,虽然ben.forta@forta.com是一个完全合法的电子邮件地址,但这个正则表达式只能匹配forta(而不是ben.forta)——别忘了,\w只能匹配字母和数字字符,不能匹配出现在字符串中间的.字符。

要想干净彻底地解决这个问题,我们需要匹配\w.。用正则表达式的术语来说,我们需要匹配字符集合[\w\.]​。下面是上面那个例子的改进版本:

正则表达式:

[\w.\+@[\w.]+\.\w+

结果:

问题似乎得到了圆满解决。​[\w.]+将匹配字符集合[\w.]​(字母数字字符、下划线和.)的一次或多次重复出现,ben.forta完全符合这一条件。考虑到有些电子邮件地址会有多层域名(或主机名)​,我们在@字符的后面也使用了一个[\w.]+

我们没有对字符集合[\w.]里的.字符进行转义。尽管如此,它还是把原始文本里的.字符匹配出来了。一般来说,当在字符集合里使用的时候,像.+这样的元字符将被解释为普通字符,不需要被转义——但转义了也没有坏处。​[\w.]的使用效果与[\w\ .]是一样的。

1.2、匹配零个或多个字符

+匹配一个或多个字符,但不匹配零个字符——+最少也要匹配一个字符。那么,如果你想匹配一个可有可无的字符——也就是该字符可以出现零次或多次的情况,你该怎么办呢?

这种匹配需要用*元字符来完成。*的用法与+完全一样——只要把它放在一个字符(或一个字符集合)的后面,就可以匹配该字符(或字符集合)连续出现零次或多次的情况。比如说,模式B.* Forta将匹配B Forta、B.Forta、Ben Forta和其他有类似规律的组合。

为了演示+*的区别,我们来看两个匹配电子邮件地址的例子。先看第一个:

文本:

Hello .ben@forta.com is my wmail address.

正则表达式:

[\w.]+@[\w.]+\.\w+

结果:

[\w.]+将匹配字符集合[\w.]​(字母数字字符、下划线和.)的一次或多次重复出现,而.ben。完全符合这一条件。这显然是一个打字错误(原始文本里多了一个.)​,但这并不是我们这里最关心的问题。问题的关键在于:虽然.是电子邮件地址里的合法字符,但把它用作电子邮件地址的第一个字符就不合法了。

一个电子邮件地址可以有任意多个字符,但它的第一个字符必须是一个字母或数字字符。根据这一要求,我们真正需要的是一个如下例所示的模式:

文本:

Hello .ben@forta.com is my wmail address.

正则表达式:

\w+[\w.]*@[\w.]+\.\w+

结果:

这个模式看起来相当复杂,但并不难理解。开头的\w+负责匹配电子邮件地址里的第一个字符(一个字母数字字符,不包括.字符)​。接下来的[\w.]*负责匹配电子邮件地址里第一个字符之后、@字符之前的所有字符——这个部分可以包含零个或多个字母数字字符和.字符。在这个例子里,解决问题的关键是能不能想到用[\w.]*来匹配字符集合[\w.]​(字母数字字符、下划线和.)的零次或多次重复出现。

1.3、匹配零个或一个字符

另一个非常有用的元字符是?​。​?只能匹配一个字符(或字符集合)的零次或一次出现,最多不超过一次——请仔细体会?与+和*的相似和区别之处。如果需要在一段文本里匹配某个特定的字符(或字符集合)而该字符可能出现、也可能不出现,​?无疑是最佳的选择。

请看下面这个例子:

文本:

Hhe URL is http://www.forta.com/, to connect securely use https://www.forta.com/ instead.

正则表达式:

http:\/\/[\w.]+\/

结果:

这个模式只匹配到了第一个URL地址(以http://开头的那个)​,没能匹配到第二个(以https://开头的那个)​。简单地在http的后面加上一个s*(s的零次或多次重复)并不能真正解决这个问题,因为那会使得httpsssss://(如此开头的URL地址显然是不合法的)也被认为是一个合法的匹配。

怎么办?看看下面这个例子就知道了——在http的后面加上一个s? :

正则表达式:

https?:\/\/[\w.]+\/

结果:

这个模式的开头部分是https?。​?在这里的含义是:我前面的字符(s)要么不出现,要么最多出现一次。换句话说,https?😕/既可以匹配http://,也可以匹配https://,但也就仅此而已。

2、匹配的重复次数

正则表达式里的+、*和?解决了许多问题,但有些问题光靠它们还不够。请思考以下问题:

  • +和*匹配的字符个数没有上限。我们无法为它们将匹配的字符个数设定一个最大值。
  • +、*和?至少匹配零个或一个字符。我们无法为它们将匹配的字符个数另行设定一个最小值。
  • 如果只使用+和*,我们无法把它们将匹配的字符个数设定为一个精确的数字。

为了解决这些问题并让程序员对重复性匹配有更多的控制,正则表达式语言提供了一个用来设定重复次数(interval)的语法。重复次数要用{和}字符来给出——把数值写在它们之间。

注意 {和}是元字符。如果需要匹配{和}本身,就应该用\对它们进行转义。不过,即使你没有对{和}进行转义,大部分正则表达式实现也能正确地处理它们(根据具体情况把它们解释为普通字符或元字符)​。话虽如此,为了避免不必要的麻烦,你最好不要依赖这种行为;在需要把{和}当做普通字符来匹配的场合,还是使用它们的转义序列{和}比较稳妥。

2.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]

下面是我们在前面用来匹配RGB值的模式,它使用了POSIX字符类:

#[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]

这两个模式本身并无不妥,但美中不足的是你不得不重复写出6次相同的字符集合(或POSIX字符类)​。下面是一个同样的例子,但我们这次将使用{ }语法来明确指定一个重复次数:

文本:

<BODY BGCOLOR="#336633" TEXT="#FFFFFF" MARGINWIDTH="0" MARGINHEIGHT="0" TOPMARGIN="0" LEFTMARGIN="0">

正则表达式:

#[[:xdigit:]]{6}

结果:

[:xdigit]匹配一个十六进制数字,{6}要求这个POSIX字符类必须连续出现6次。类似地,使用模式#[0-9A-Fa-f]{6}也可以解决这个问题。

2.2、为重复匹配次数设定一个区间

{}语法还可以用来为重复匹配次数设定一个区间——也就是为重复匹配次数设定一个最小值和一个最大值。这种区间必须以{2, 4}这样的形式给出——{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}

结果:

注意,上面这个例子里的模式并不能检查日期值是否有效;诸如54/67/9999之类的无效日期也能通过这一测试。它只能用来检查日期值的格式是否正确(这一环节通常安排在日期值本身的有效性检查之前)​。

注意 重复次数可以是0。比如,{0, 3}表示重复次数可以是0、1、2或3。

2.3、匹配“至少重复多少次”

{ }语法的最后一种用法是给出一个最小的重复次数(但不必给出一个最大值)​。{ }的这种用法与我们用来为重复匹配次数设定一个区间的{ }语法很相似,只是省略了最大值部分而已。比如说,{3, }表示至少重复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}

结果:

这个例子里的原始文本来自一份报表,它的第一列是定单号,第二列是定单金额。我们构造的正则表达式首先使用了一个\d+:来匹配定单号(这部分其实可以省略——我们可以只匹配金额部分而不是匹配包括定单号在内的一整行)​。模式\$\d{3, }\.\d{2}用来匹配金额部分:\$匹配$\d{3, }匹配至少3位数字(也就是所有大于或等于$100美元的金额)​、\.匹配.\d{2}匹配小数点后面的两位数字。整个模式从7条记录里正确地匹配到了4条符合要求的记录。

3、防止过度匹配

?只能匹配零个或一个字符,{n}和{m, n}也有一个重复次数的上限;换句话说,这几种语法所定义的“重复次数”都是有限的。但本章介绍的其他重复匹配语法在重复次数方面都没有上限值,而这样做有时会导致过度匹配的现象。

到目前为止,我们选用的例子都不存在过度匹配的问题,但你迟早会遇到类似于下面这个例子的情况。这个例子里的原始文本来自一个Web页面,其中包含着两个HTML<B>标签;而我们的任务是用一个正则表达式把那两个<B>标签里的文本匹配出来(为了对这些文本进行替换或排版等)​。下面就是这个例子:

文本:

This offer is not available to customers living in <B>AK</B> and <B>HI</B>.

正则表达式:

<[Bb]>.*<\/[Bb]>

结果:

<[Bb]>匹配<B>标签(大小写均可),</[Bb]>匹配</B>标签(也是大小写均可)​。但这个模式只找到了一个匹配而不是预期中的两个:第一个<B>标签之后、最后一个</B>标签之前的所有东西——AK</B> and <B>HI——被.*一网打尽。虽然没有漏掉我们想要匹配的文本,但问题是第2个<B>标签不明不白地“失踪”了。

为什么会这样?因为*+都是所谓的“贪婪型”元字符,它们在进行匹配时的行为模式是多多益善而不是适可而止的。它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾,而不是从这段文本的开头匹配到碰到第一个匹配时为止。

在不需要这种“贪婪行为”的时候该怎么办?答案是使用这些元字符的“懒惰型”版本(​“懒惰”在这里的含义是匹配尽可能少的字符——与“贪婪型”元字符的行为模式刚好相反)​。懒惰型元字符的写法很简单,只要给贪婪型元字符加上一个?后缀即可。下表列出了几个常用的贪婪型元字符和它们的懒惰型版本。

*?*的懒惰型版本;下面是使用*?来解决刚才那个例子的做法:

正则表达式:

<[Bb]>.*?<\/[Bb]>

结果:

问题得到了圆满解决。因为使用了懒惰的*?,第一个匹配将仅限于AK,原始文本里的<B>HI</B>成为了第二个匹配。

返回列表