使用子表达式
1、什么是子表达式
我们在前面学习了如何匹配一个字符的连续多次重复。正如我们讨论的那样,\d+将匹配一个或多个数字字符,而https?😕/将匹配http://或https://。
在这两个例子里(事实上,是在我们此前见过的所有例子里),用来表明重复次数的元字符(如?或*或{2},等等)只作用于紧挨着它的前一个字符或元字符。
我们来看一个例子。有些短语(例如Windows 2000)虽然由多个单词构成,但其实是一个整体。有许多HTML程序员喜欢让这类短语在浏览器里显示在同一行上。为了确保这一点,他们会在编写HTML文档时在这些短语的单词之间使用非换行型空格( , nbsp是"non-breaking space"的缩写,其含义是"不是换行符的空格")而不是普通的空格。下面就是一个这样的例子:
文本:
text
Hello, my name is Ben Forta, and I am
the author of books on SQL, ColdFusion, WAP,
Windows 2000, and other subjects.
正则表达式:
reg
{2,}
结果:

 ;是HTML语言中的非换行空格字符。在这里使用模式 {2, }的本意是希望它能把 ;连续两次或更多次的重复出现找出来,但它没能给出我们所预期的结果。为什么会这样?因为{2, }只作用于紧挨着它的前一个字符------那是一个分号。如此一来,这个模式只能匹配像 ; ; ; ;这样的文本,但无法匹配 。
2、子表达式
这就引出了子表达式的概念。子表达式是一个更大的表达式的一部分;把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当作一个独立元素来使用。子表达式必须用(和)括起来。
提示(和)是元字符。如果需要匹配(和)本身,就必须使用它的转义序列\(和\)。
为了演示子表达式的用法,我们来看看刚才的那个例子:
文本:
text
Hello, my name is Ben Forta, and I am
the author of books on SQL, ColdFusion, WAP,
Windows 2000, and other subjects.
正则表达式:
js
( ){2,}
结果:

( )是一个子表达式,它将被视为一个独立元素,而紧跟在它后面的{2, }将作用于这个子表达式(不仅仅是分号)。这个模式解决了我们的问题。
我们再来看一个例子,这次是用一个正则表达式来查找IP地址。IP地址的格式是以英文句号分隔的四组数字,例如12.159.46.200。因为每组数字由1个、2个或3个数字字符构成,所以这4组数字可以统一使用模式\d{1, 3}来匹配。下面就是这个例子:
文本:
js
Pinging hog.forta.com [12.159.46.200]
with 32 bytes of data:
正则表达式:
reg
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
结果:

\d{1, 3}在这个模式里重复了4次,它们分别匹配IP地址里的一组数字。IP地址里的4组数字由.分隔,该字符由模式里的转义序列\.负责匹配。
稍微留意一下就会发现,在这个例子里,模式\d{1, 3}\ .(最多3个数字字符、后面跟着一个.)连续出现了3次,它同样可以被表达为一个重复。下面是这个例子的另一种解决方案:
正则表达式:
reg
(\d{1,3}\.){3}\d{1,3}
结果:

这个模式与前面那个有着同样的效果,但我们这次使用了另一种语法:先用(和)把表达式\d{1, 3}\ .括起来使它成为一个子表达式,再用\d{1, 3}\ .){3}把这个子表达式重复了3次(它们对应着IP地址里的前3组数字),最后面的\d{1, 3}用来匹配IP地址里的最后一组数字。
注意 在上面这个例子里,使用(\d{1, 3}\ .){4}作为模式是不妥当的。你能分析出为什么不能用它来解决这个问题吗?
为了提高可读性,有不少用户喜欢给表达式的每一个子表达式都加上括号。比如,把上面那个例子里的模式写成(\d{1, 3}\ .){3}(\d{1, 3})。这种做法在语法上完全成立,对表达式的实际行为也没有任何不良影响(但视乎具体的正则表达式实现,这对匹配操作的速度可能会有点儿影响)。
子表达式是一个非常重要的概念,所以我们认为有必要再给大家看一个例子,它不涉及重复次数问题。在下面的例子里,我们的任务是把一条用户记录里的年份数字完整地匹配出来:
文本:
text
ID: 042
SEX: M
DOB: 1967-08-17
Status: Active
正则表达式:
reg
19|20\d{2}
结果:

这个例子需要我们构造一个模式去查找一个4位数的年份数字。为了排除没有实际意义的结果,我们把前两位数字限定为19和20。这个模式里的|字符是正则表达式语言里的或操作符,19|20将匹配数字序列19或20。既然如此,模式19|20\d{2}应该匹配以19或20开头的4位数字(19或20的后面再跟着两位数字)。可是,这个模式的匹配结果与我们的预期并不相符,它只匹配到了19,随后两位数字没有被匹配到。为什么会这样?因为|操作符是把位于它左边和右边的两个部分都作为一个整体来看待的,它会把模式19|20\d{2}解释为19或20\d{2}(也就是把\d{2}解释为以20开头的那个表达式的一部分)。换句话说,它将匹配数字序列19或以20开头的任意4位数字。最终的结果你们已经看到了,它只匹配到了19。
这个例子的正确答案是把19|20归为一个子表达式,如下所示:
正则表达式:
reg
(19|20)\d{2}
结果:

我们把所有的选项都归纳到了一个子表达式里,这将向|表明我们打算匹配的是这个子表达式里的选项之一。(19|20)\d{2}正确地匹配到了1967;当然,以19或20开头的任何一个4位数字都将与这个模式相匹配。今后(比如,从现在算起100年内),如果需要修改这段代码以包括以21开头的年份,只要把这个模式改成(19|20|21)\d{2}就可以了。
3、子表达式的嵌套
子表达式允许嵌套。事实上,子表达式允许多重嵌套,这种嵌套的层次在理论上没有限制,但在实际工作中还是应该遵循适可而止的原则。
多重嵌套的子表达式可以构造出功能极其强大的正则表达式来,但那难免会让模式变得难以阅读和理解,而这也正是很多人觉得正则表达式难以学习和掌握的原因之一。这种表面现象掩盖了这样一个事实:绝大多数嵌套子表达式都没有它们看上去那么复杂。
为了演示嵌套子表达式的用法,我们再去看看刚才那个匹配IP地址的例子。下面是我们刚才使用的模式(先是一个连续重复3次的子表达式,然后是最后一组数字):
文本:
js
Pinging hog.forta.com [12.159.46.200]
with 32 bytes of data:
正则表达式:
js
(\d{1,3}\.){3}\d{1,3}
结果:

这个模式有什么不对的地方吗?从语法上讲,它完全正确。IP地址由四组数字构成,每组数字由1到3个数字字符构成,它们之间以英文句号分隔。说这个模式正确,是因为所有合法的IP地址都与之相匹配。但深入研究一下就会发现,这个模式还可以匹配其他一些东西;说得明白点儿,不合法的IP地址也能与之相匹配。
IP地址由4个字节构成,IP地址中的4组数字分别对应着那4个字节,所以IP地址里的每组数字的取值范围也就是单个字节的表示范围,即0~255。这意味着IP地址里的每一组数字都不能大于255,可是上面那个模式还能匹配诸如345、700、999之类的数字序列,而这些数字在IP地址里都是非法的。
如果有办法设定各种取值范围的话,事情会简单得多,但可惜的是正则表达式只是一种工具,而且还是一种不懂数学运算的工具,它们在匹配字符的时候并不真正关心那些字符到底是什么以及有什么含义。你的数学能力再好在这里也帮不上忙。
真的没有解决这个问题的办法吗?未必,只要你们能够充分发挥你们的逻辑思维能力,就能解决与正则表达式有关的任何难题。这里的基本思路是:在构造一个正则表达式的时候,一定要把你想匹配什么和你不想匹配什么详尽地定义清楚。下面是一个合法的IP地址里的各组数字必须且只能符合的规则,我们随后将根据这些规则来构造一个相应的模式:
- 任何一个1位或2位数字。
- 任何一个以1开头的3位数字。
- 任何一个以2开头、第2位数字在0~4之间的3位数字。
- 任何一个以25开头、第3位数字在0~5之间的3位数字。
像这样把所有的正则全部罗列出来之后,构造一个同时符合所有原则的模式的具体步骤也就清晰了。下面是这个例子的继续:
正则表达式:
js
(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))
结果:

这个模式的使用效果显而易见,但它还是需要仔细研读才能看明白。这个模式由一系列嵌套子表达式构成。我们先来说说由4个子表达式构成的(((\d{1, 2})|(1\d{2})|(20-4\d)|(250-5)\ .):(\d{1, 2})匹配任意一位或两位数字(0 ~ 99);(1\d{2})匹配以1开头的任意三位数字(100 ~ 199);(20-4\d)匹配整数200 ~ 249;(250-5)匹配整数250 ~ 255。这几个子表达式通过|操作符结合为一个更大的子表达式(其含义是只需匹配这4个子表达式之一即可)。随后的\ .用来匹配.字符,它与前4个子表达式构成的子表达式又构成了一个更大的子表达式(4组数字选项和\ .),而接下来的{3}表明需要重复3次。最后,数值范围又重复了一次(这次省略了尾部的\ .)以匹配IP地址里的最后一组数字。通过把4组数字的取值范围都限制在0~255之间,这个模式准确无误地做到了只匹配合法的IP地址、不匹配非法的IP地址。
上面这个例子里的正则表达式看起来很难理解。把它们弄明白的关键是要把它们分解开、每次只分析和理解一个子表达式。在分析各个子表达式的时候,应该按照先内后外的原则来进行而不是从第一个字符开始一个字符一个字符地去尝试。你有过几次这样的经验之后就会发现,嵌套子表达式并不像它们看上去那么复杂。