使用子表达式
在这一部分将要学习如何运用子表达式的概念对表达式进行分组和归类。
什么是子表达式
之前学习的用来表明重复次数的元字符(如?或*或{2}等),都是只作用于紧挨着它的前一个字符。
举个例子,在HTML中会使用
来表示空格,如果我们想匹配一个HTML文档中的连续两个以上的空格,我们会写出如下表达式:
{2,}
但是我们发现不会得到预期的结果,原因是{2,}只会作用于紧挨着它的前一个字符,也就是一个分号。因此这个模式只能匹配 ;;;;;
这样的文本。
子表达式
这就引出了子表达式的概念,子表达式是一个更大的表达式的一部分,把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当做一个独立元素来使用。
子表达式必须用(和)括起来。
(和)是元字符,如果需要匹配其本身则需要转义
回到刚才的例子,我们可以改写成:
( ){2,}
( )
是一个子表达式,它将被视为一个独立元素,而紧跟在他后面的{2, }将作用于这个子表达式整体。
再看一个例子,要写出一个用来匹配IP地址的正则表达式。IP地址是以英文句号分割的4组数字,每一组数字为1到3位,因此可以这么表示:
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
稍微留意一下可以发现,\d{1,3}\.
这个模式连续出现了三次,他同样可以被表达为一个重复,因此可以使用另一种解决方案:
(\d{1,3}\.){3}\d{1,3}
再看一个例子,我们要匹配4位数字所代表的年份:
19|20\d{2}
|是正则表达式中的或操作,表示年份的前两位是19或20,而\d{2}代表后面时任意两位数字。但是发现并不奏效。原因是|操作符会分别把左边当做一个整体,右边当做一个整体,因此它会把上面的模式解释为19,或者20\d{2},这样的话就只能匹配数字19和20开头的4位数字了。
用子表达式做一下更改:
(19|20)\d{2}
这样就能正确得到匹配结果了。
子表达式的嵌套
子表达式允许多重嵌套。
再回到刚才匹配IP地址的例子,其实是不太准确的,因为IP地址中的每一个数字范围都是0~255,但是我们刚才的写法并没有对此作出限制。那么如何利用子表达式进行改进呢?
可以这么考虑,每一个数字可能是以下四种情况之一:
- 一位或两位数字
- 以1开头的三位数字
- 以2开头的三位数字,且第2位位于0~4之间
- 以25开头的三位数字,且第3位位于0~5之间
像这样把所有的正则罗列出来之后,构造一个同时符合所有原则的步骤也就很清晰了:
(((\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]))
(\d{1,2})
匹配一位或两位数字(1\d{2})
匹配以1开头的三位数字(2[0-4]\d)
匹配以2开头的三位数字,且第2位位于0~4之间(25[0-5])
匹配以25开头的三位数字,且第3位位于0~5之间
这四个表达式通过|构成了一个更大的子表达式。
回溯引用
回溯引用有什么用
先看个例子,比如我们想要匹配HTML页面中的各级标题(<h1>
到<h6>
),容易写出以下模式:
<h[1-6]>.*?</h[1-6]>
但是这个模式其实有一个问题,那就是<h2>sssss</h3>
也会被匹配,但是明显这是一个不合法的标题。
出现这种情况的根本原因是这个模式用来匹配结束标签的那个部分对这个模式用来匹配开始标签的那个部分毫无所知。要彻底解决这个问题只能求助于回溯引用。
回溯引用匹配
我们先看另一个例子,比如我们想在一段文本里找出所有连续重复出现的单词,那么显然在搜索某个第二次出现的单词时,这个单词必须是已知的。
回溯引用允许正则表达式模式引用前面的匹配结果。
回溯引用指的是模式的后半部分引用在前半部分中定义的子表达式。
看一下下面的模式:
[]+(\w+)[]+\1
其中,[]+
用于匹配一个或多个空格,\w+
用于匹配一个或多个字母数字字符,但是这里给它加上了括号,这并不是要应用在重复匹配中的,而是把整个模式的一部分单独划分成一个子表达式以便在后面引用。
这个模式最后一个部分是\1,这是一个回溯引用,而它的引用正是前面划分出来的那个子表达式:当(\w+)匹配到单词of时,\1也匹配of;(\w+)匹配and时,\1也匹配and。
因此总结起来,上面的模式就能匹配连续的两个重复单词(单词前后至少有一个空格)。
那么,\1到底代表什么?它代表着模式里的第一个字表达式,\2代表着第二个字表达式,\3代表第三个,以此类推……。
其实可以把回溯引用想象成变量
我们现在回过头来想匹配<h1>
标签的例子。利用回溯引用,构造一个模式去匹配任何级别标题的开始标签和与之配对的结束标签:
<(h[1-6])>.*?</\1>
用括号把h[1-6]括起来,使它成为一个子表达式,这样一来,就可以在匹配结束标签的模式中引用这个子表达式了。
回溯引用只能用来引用模式里的子表达式
-
回溯引用通常从1开始计数。
在许多实现里,第0个匹配\0可以用来代表整个正则表达式。
回溯引用在替换操作中的应用
正则表达式大部分是用来执行搜索的,但是其实也可以完成各种复杂的替换操作。
正则表达式适用于复杂的替换,尤其是需要使用回溯引用的场合。
比如,电话号码被保存为313-555-1234,现在我们想要把其重新排版为(313)555-1234。
正则表达式
(\d{3})(-)(\d{3})(-)(\d{4})
替换
($1)$3-$5
我们用 来进行替换,与回溯引用语法相同, 1代表第一个子表达式,$3代表第三个子表达式。
在js中应用回溯语法来进行替换,需要把\换成$
大小写转换
有些正则表达式允许我们使用以下元字符对字母进行大小写转换:
- \E:结束\L或\U转换
- \l:把下一个字符转换为小写
- \L:把\L到\E之间的字符全部转换为小写
- \u:把下一个字符转换为大写
- \U:把\U到\E之间的字符全部转换为大写
\l和\u只能把下一个字符(或子表达式)转换为小写或大写。
\L和\U则把它后面所有字符都转换为小写或者大写,直到遇上\E为止。
下面是一个例子,把<h1>
和</h1>
之间的文字转换为大写:
(<h1>)(.*?)(</h1>)
替换
$1\U$2\E$3
首先模式把一级标题分成了三个子表达式:开始标签、标题文字、结束标签。
第二个模式再把文本重新组合起来。\U$2\E
把第二个子表达式(标题文字)转换为大写。
前后查找
有时我们需要用正则表达式标记要匹配的文本的位置,这就引出了前后查找的概念。
前后查找
先看一个例子,要把一个html页面的<title>
标签之间的内容提取出来,我们可能会这么写:
<title>.*?</title>
但是这样的话除了标签之间的内容title的开闭标签也会一并被返回,但这并不是我们需要的。
办法之一是使用子表达式。我们可以利用子表达式将被匹配文本划分为三个部分:开始标签、标题文字、结束标签。把被匹配文本划分为多个部分之后,从他们当中提取且只提取出我们需要的东西就可以了。
但是这种方法一开始就是明知自己不需要的东西还是要检索出来,显得毫无意义。
因此我们真正需要的是一个包含的匹配本身并不返回,而是用于确定正确的匹配位置,它并不是匹配结果的一部分。
因此前后查找就这样出现了。
向前查找
向前查找指定了一个必须匹配但不在结果中返回的模式。
向前查找实际就是一个子表达式。
从语法上看,一个向前查找模式其实就是一个以?=开头的子表达式,需要匹配的文本跟在=的后面。
比如,我们想要提取URL地址中的协议部分,可以使用下面这个模式:
.+(?=:)
在URL地址中,协议名与主机名之间以一个:分隔。模式.+
匹配任意文本。
这样的模式最终能够匹配URL中的协议:http、https、ftp
但是:并没有出现在最终结果里。我们用(?=:)
表示只要找到:就行了,不要把它包括在最终的匹配结果里。
任何一个子表达式都可以转换为一个向前查找表达式,只要给他加上一个?=前缀即可。
在同一个搜索模式里可以使用多个向前查找表达式,他们可以出现在模式里的任意位置。
向后查找
?=
将向前查找,也就是查找出现在被匹配文本之后的字符,但是不消费那个字符。
除了向前查找,许多正则表达式还支持向后查找,也就是查找出现在被匹配文本之前的字符,但是不消费他。向后查找操作符是?<=
举个例子,比如我们想匹配所有的美元数字,形式是
223.4,但是我们不想匹配美元符号
,这就可以应用向后匹配。
(?<=\$)[0-9.]+
(?<=\$)
只匹配
,但不消费它。因此最终的匹配结果里只有数字而没有
字符。
向前查找模式的长度是可变的,它可以包含.和+之类的元字符,所以非常灵活。
而向后查找模式只能是固定长度。
结合向前查找和向后查找
向前查找和向后查找可以组合在一起使用。回到最初的那个例子,要把一个html页面的<title>
标签之间的内容提取出来,可以组合使用向前查找和向后查找:
(?<=<title>).*?(?=</title>)
问题解决了,最前面是一个向后匹配操作,它匹配<title>
但不消费,同理最后面是向前匹配。最终返回的匹配结果包含且仅包含标题文字。
为减少歧义,可以把第一个要匹配的字符<进行一个转义,也就是写成
(?<=\<
对前后查找取非
向前查找和向后查找通常用来匹配文本,其目的是为了确定将被返回为匹配结果的位置(通过指定匹配结果的前后必须是哪些文本)。这种用法被称为正向前查找和正向后查找。
前后查找还有一种不太常见的用法叫做负前后查找。负向前查找将向前查找不与给定模式相匹配的文本,负向后查找则向后查找不与给定模式相匹配的文本。
我们知道^可以进行取非操作,但其不能对前后查找进行取非处理,而是必须使用另外一种语法:前后查找必须用!来取非(它将替换掉=)。
下面列举的是所有前后查找操作符:
- (?=) 正向前查找
- (?!) 负向前查找
- (?<=) 正向后查找
- (?
\b(?<!\$)\d+\b
最前和最后的\b用于定义单词边界。我们使用(?<!\$)
表示只匹配那些不以$开头的数值,如50,22等。