RegExp Reference

Sarkuya,

概述

功能

正则表达式可快速地找出特定的子字符串。利用其强大的模式,我们可以指定非常丰富的匹配条件。除了查找,在替换时还可以使用模式,以简短的代码高效地实现替换功能。

正则表达式有各种各样的流派。各流派有其稍微不同的语法及功能。JavaScript的正则表达式以强大的Perl 5为原型。

JavaScript使用RegExp对象来处理正则表达式的任务。

学习正则表达式的路线

正则表达式的功能很强大,但内容不多,可按下面的路线来学习。

  1. RegExp的exec()方法,先使用"gi"标志
  2. 模式,配合以各类标志
  3. 匹配引擎匹配原理
  4. 匹配实练

配合以一个在线匹配的工具,能达到事半功倍的效果。

RegExp构造方法

var re1 = RegExp("abc", "gi");      // 以函数调用作为构造方法
var re2 = RegExp(re1);              // 未指定flags,但由于re是一个RegExp对象,也以re1的flags来构造re2

var re3 = new RegExp("abc");        // 标准构造函数
var re4 = new RegExp(re3, "gim");

var re5 = /abc/gi;                  // 字面符形式

RegExp的属性

RegExp的属性有:

source
被搜索的字符串
global
是否全局搜索。根据标志"g"来设定。
ingnoreCase
是否忽略大小写。根据标志"i"来设定。
multiline
是否搜索多行。根据标志"m"来设定。
lastIndex
下次搜索要开始的索引位置。

RegExp的方法

RegExp的方法有:

exec(string)
根据pattern来被搜索string。如果匹配成功,返回一个包含匹配结果的数组对象。如果匹配不成功,返回null.
test(string)
根据pattern来被搜索string。如果匹配成功,返回true。否则返回false。

模式

有直接的字符模式。也有以特殊字符表示特定含义的模式。

特殊字符

也称为模式字符,有:

  1. ^
  2. $
  3. \
  4. .
  5. *
  6. +
  7. ?
  8. (
  9. )
  10. [
  11. ]
  12. {
  13. }
  14. |

注意,HTML中的“<”及“>”及“/”均不是正则表示式中的特殊字符。

但在JavaScript代码中,如果使用字面符的构造方法,因为这种方式使用了"/"符号,因此如果需匹配此字符,则需对其对行转义:

var re = /abc\//gi;

若使用字符串来构造RegExp,则需注意对"\"进行转义

var re = new RegExp("a\\sb", "gi");

正常情况下,"\s"代表空字符,但当将其放进字符串中时,它被解释为一个字符"\"后面跟有一个字符"s",被解释为两个单独的字符。而我们将其变为"\\"后,即告诉JavaScript, 请对此字符进行转义,它不再是一个普通的字符,而是一个特殊的字符,此字符与后面的"s"连在一起,构成一个有特定含义的特殊的字符。

因此,无论是采用哪种构造方法,即使有些字符不是正则表达式的特殊字符,也要进行相应的转义。

字符类转义 (CharacterClassEscape)

通过"\"加上特定字符,表示特定的含义。

  1. \d
  2. \D
  3. \s
  4. \S
  5. \w
  6. \W

控制字符类转义 (ControlEscape)

通过"\"加上特定字符,表示特定的控制字符。

  1. \f
  2. \n
  3. \r
  4. \t
  5. \v

特殊字符表示的模式

字符类

字符含义
.

1. 单个字符。但不包含行符:\n, \r, \u2028, \u2029

2. 在字符集中,不是特殊字符,而只是一个直接字面符"."

\d 阿拉伯数字。等同于[0-9]
\D 非阿拉伯数字。等同于[^0-9]
\w 等同于[A-Za-z0-9_]。即字母,数字,下划线。
\W 等同于[^A-Za-z0-9_]。即非字母,数字,下划线。
\s 单个空白符。等同于[ \f\n\r\t\v\u00a0\u1680\2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
\S 非空白符。
\t 水平tab
\r 回车
\n 换行
\v 垂直tab
\f form-feed
[\b] 退格键
\0 NUL字符
\cX X为A-Z. 控制字符
\xhh 两位十六进制字符
\uhhhh 四位十六进制的UTF-16代码单元
\u{hhhh}或\u{hhhhh} U+hhhh或U+hhhhh的Unicode. 标志u设置了才有效
\ 表示后面的字符将被转义。非特殊字符将被转义为特殊字符,特殊字符将被转义为非特殊字符。

断言

字符含义
x(?=y) Lookahead assertion. 如果x的后面紧随着y, 则x匹配成功。
x(?!y) Negative lookahead assertion. 如果x的后面没有紧随着y, 则x匹配成功。
(?<=y)x Lookbehind assertion. 如果x的前面有y, 则x匹配成功。
(?<!y)x Negative lookbehind assertion. 如果x的前面没有y, 则x匹配成功。

Lookbehind于ECMAScript 2018 Specification中予以规范,但目前只有Google Chrome支持这个功能。

边界

字符含义
^ 是否以特定字符开始。如果多行标志"m"设为true, 则紧随在每个断行符之后的字符也能匹配上。若出现在分组符号“[]”中,则表示“非”的意思。
$ 是否以特定字符结束。如果多行标志"m"设为true, 则紧随在每个断行符之前的字符也能匹配上。
\b

单词分界符。前面或后面是否以单词的形式出现。即前或后是否带有空白符。注意,如果匹配成功,"\b"不计入匹配的长度。即,"\b"的匹配长度为0。

易混淆:[\b],匹配退格键。

\B 非单词分界符。"\B"不计入匹配的长度。

分组与范围

字符含义
x|y x或y
[xyz], [a-c] 字符集。
[^xyz], [^a-c] 不在范围内的字符集。
(x) 捕获组。索引下标依序为[1], [2]... [n-1],括号无论是否嵌套,均从左开始计算左括号"("。或者($1, ... $9)
\n 向后引用(back reference). n为正数。即引用前面捕获组中第n个变量。
(?<Name>x) 命名捕获组。对于/\((?<area>\d\d\d)\)/,可通过matches.groups.area来访问。
(?:x) 非捕获组。匹配x但不记忆捕获结果。

数量

字符含义
x* 0或多。
x+ 1或多。
x? 0或1。
x{n} n次。n为正整数。
x{n,} 至少n次。
x{n,m} 至少n次,至多m次。
x*?
x+?
x??
x{n}?
x{n,}?
x{n,m}?
默认情况下,当出现*, +, ?时,正则表达式引擎是贪婪(greedy)的。意味着它将尽量匹配更多的字符串。在这些字符后面加上一个?号,使它们不再贪婪。此时,当找到一个匹配时,就立即停止。

标志 (flags)

g, i, m.

搜索引擎工作机制

基本机制

我们准备进行下面搜索:

String:  This is the story about the world.
Pattern: th

使用的代码:

var string = "This is the story about the world.";
var regExp = /th/gi;

var result;

while ((result = regExp.exec(string)) !== null) {

}

首先,当创建regExp时,regExp.source等于/th/gi,regExp.lastIndex值为0,表示下次匹配将从string的索引值为0的位置开始。此时,引擎已经做准备,处于等待指令状态。

接着,我们发出指令:regExp.exec(string)。引擎完成以下工作:

1. 调用regExp.exec(string)。由于regExp.lastIndex为0,引擎开始在string中index为0的位置进行搜索"th"。

2. 前面两个字符"Th"即匹配成功。引擎创建一个数组,设为array。array[0]记录下所找到的部分"Th"。array.index存储此轮所匹配到的索引位置。array.input存储被搜索的字符串,这里即"This is the story about the world."。若有捕获组,则以array[1]...array[n-1]分别存储所有捕获组的匹配内容。

3. 根据此轮所匹配到的array[0]中的内容的长度,将regExp.lastIndex的值设为arrya.index + array[0].length,将指针指向下轮匹配开始的位置。

4. 返回array.

上面是引擎在调用regExp.exec(string)时内部一个完整的流程,我们可称其为第1轮的匹配成功。返回array后,程序流程转移到程序员手中。在上面的代码中,我们先将其赋值于名为result的对象,然后判断result是否为null。如果为null,说明引擎已经全部搜索完毕,退出while循环。如果不为null值,则可在while循环中根据result及regExp的相应属性值进行进一步的处理。

上一步,由于result不为null,while指令再次调用regExp.exec(string)进行搜索,开始了第2轮的匹配。由于第1轮在string的索引值0,1的位置成功匹配到"Th",regExp.lastIndex的值更新为2,因此第2轮的匹配将从string中索引值为2的地方开始搜索。此时,重复上述1—4步。

正则表达式多数情况下是在一个内容较多的字符串中搜索特定内容的子字符串,因此,将导致可能的多轮匹配。每调用一次regExp.exec(string),就是进行了一轮的匹配。显然,由于在string中共有3个"th"出现(不计大小写),因此,上面的代码共调用3次regExp.exec(string),也即共进行了3轮的匹配。当3轮匹配完成后,regExp.lastIndex自动重置为0。这时,我们可以说,搜索完毕,共有3次匹配成功。

下面是3轮匹配成功的图示。

Turn 1:
  [0]: This is the story about the world.

Turn 2:
  [0]: This is the story about the world.

Turn 3:
  [0]: This is the story about the world.

"[0]"表示以黄色加亮的部分是result[0]存储的内容。

只有当模式的标志中设有"g",即全局搜索时,引擎才会自动更新regExp.lastIndex。我们才可根据这个自动更新的值进行循环调用regExp.exec(string)。否则,程序将陷入死循环。

贪婪的引擎

准备搜索:

String:  This is a real story.
Pattern: th(is )*

模式th(is )*的意思是:

  1. 字符t, 字符h
  2. 后面跟着0或多的"is "。

因为比较简单,我们先用肉眼观察。

首先,"Th"能匹配成功。它符合"th"后面跟着数量为0的"is "的条件。

其次,"This "能匹配成功。它符合"th"后面跟着数量为1的"is "的条件。

第三,"This is "能匹配成功。它符合"th"后面跟着数量为多(具体为2个)的"is "的条件。

我们想当然地认为,引擎应进行3轮匹配,依次返回"Th", "This ", "This is "的结果。但实际结果不是。从上节我们知道,每轮匹配,引擎都会更新regExp.lastIndex的值,以指向下一次匹配开始的位置。在"Th"匹配成功后,regExp.lastIndex的值会更新为2,为第2轮的开始位置。此时指向"i",在这个位置上,再匹配"th(is )*",显然不会再次匹配成功。这就漏掉了后面两种匹配结果,跟我们的预期结果不相符。

因此,每轮匹配,只会开始于regExp.lastIndex的不同值的地方。而我们这次遇到的问题的,在索引位置为0的地方,同时出现了3种匹配结果。既然索引位置都是0,则针对此位置只会有一轮的匹配,而不是3轮的匹配。

上节我们还知道,此轮匹配成功的结果存将存放于result[0]中返回。那么,result能否同时告诉我们这轮匹配的3种结果?result[1]...result[n-1]是捕获组的信息,这里无法指望它们。只有result[0]才存放这次匹配成功的结果,且它只能存放一个匹配信息,要么是"Th", 要么是"This ", 要么是"This is "。

引擎说,我是很贪婪的,遇到这种情况,我会尽可能地往后搜索,只有找到最后一个匹配结果后,我才停止本轮的匹配。

具体来说,其细节是:regExp.lastIndex为0。先匹配到"Th",引擎先暂记下这个匹配结果"Th"及其位置。然后,还是根据regExp.lastIndex,依旧从索引为0的地方开始,从string中取出更多的字符,以查看还有没有符合条件的其他匹配。此次匹配成功"This "。再次记下此匹配结果及位置。第3次,从头开始,再取更多的字符,匹配"This is "成功。当遇到"a"时,已经不再符合模式的条件,可以结束本轮匹配。于是,引擎将regExp.lastIndex的值设为"a"所在的位置,即8,将result[0]的值设为最后一次匹配结果"This is ",停止本轮搜索,从regExp.exec(string)返回。因此,我们看到的是这样的结果:

Turn 1:
  [0]: This is the story about the world.
  [1]: is

由于模式中有括号,result[1]也存储了这个捕获结果。

特殊字符中的表示数量的字符,如?, *, +, 都会产生这种贪婪的效果。

引擎的这种贪婪的效果,有时不是我们所要的。对于字符串:

<p><span>abc</span></p><p></p>

模式:

<(\w+)>.*</\1>

将匹配所有的字符:

Turn 1:
  [0]: <p><span>abc</span></p><p></p>
  [1]: p

既不能只匹配出嵌套的span,也不能只匹配出左边的一对p,或只匹配出右边的一对p。相反,只要最左边有"<p>",最右边有"</p>",它就全部打包进匹配结果,而不在乎这里面可能含有的特殊结构。根据引擎的贪婪特性,它这样做是对的,如果说达不到我们的预期效果,只能说是我们错了。我们得改用其他的模式。

匹配实例

匹配HTML

目标:匹配HTML标签名,如p, span等。

需考虑的几种情况:

  1. 开始标签与结束标签
  2. 标签名称与标签的众多属性
  3. 嵌套标签

简单实例

input:

<p></p>

这个HTML标签由开始标签<p>,以及结束标签</p>组成。开始标签有一个小于号"<",p, 一个大于号">"。结束标签类似于开始标签,只不过p前面多了一个斜杠"/"。若要同时匹配这两个p,可将这个"/"视为数量为0或1的可选项。因此,模式

</?p>

就可同时匹配出"<p>",及"</p>"。而我们只希望匹配HTML的标签名,不要表示HTML标签的其他标志,则可通过捕获组的方式将"p"括起来。

</?(p)>

这样,就可匹配出符合HTML标签格式的两个p。它们存在于每次匹配的结果result[1]中。

匹配带有纯文本内容的段落

input:

<p>abc</p>

开始标签与结束标签中带有文本"abc"。上一节的例子采取可选值的方式,同时匹配开始标签与结束标签,但忽略了开始标签与结束标签的顺序,因此不适合于这种情况。由于开始标签及结束标签中的名称都是一样的,这里都是“p”,因此,可使用back reference的方式。模式:

<(p)>.*</\1>

匹配出整个字符串"<p>abc</p>"(result[0]),以及第一个“p”(result[1])。若要用"\1"来向后,即往左,引用先前已经匹配好的结果,必须将此匹配结果先用括号围绕起来,因此,这个模式中的括号在此不可省略。这个结果正好也是我们想要匹配出来的。但结束标签后面的p并未匹配出来。好办,只需将其也用括号围绕起来就行了。

<(p)>.*</(\1)>

这同时也匹配出第二个p,位于result[2]中。

匹配带有嵌套标签的段落

input:

<span>abc<span>Hello</span>xyz</span>

p的内容中即有纯文本,还嵌套了一个span标签。正则表达式的引擎是从左到右匹配。

模式:

</?([^>]+)>

匹配出四个span,存在result[1] - reulst[4]中。因为?, *, +的greedy性,而导致引擎backtrack。因使用这种否定的方式,当含有有效的HTML代码时,不再有backtrack。

模式:

</?([^>\s]+)\s?[^>]*>

匹配出HTML标签名称存在于result[1]中。它的含义是,<, 然后可能带有/,然后有1或多个既不是>,也不是空白符的字符集,然后可能带有空格,空格后面是0或多个不是>的字符集。

这样,对于input:

<p class='m' width='23'><em>abc<span>Hello</span>xyz</em></p>

p的后面带有属性值,也能正确地匹配出pemspan

匹配标签的多个属性

Input:

<div width='20' height='30' color='red'>

HTML标签中每个属性的特点是,前面有一个空格,然后是名值组。

模式

\s(\w+)='(\w*)'

匹配出3个属性的名值组。

Turn 1:
  [0]: <div width='20' height='30' color='red'>
  [1]: width
  [2]: 20

Turn 2:
  [0]: <div width='20' height='30' color='red'>
  [1]: height
  [2]: 30

Turn 3:
  [0]: <div width='20' height='30' color='red'>
  [1]: color
  [2]: red

每次匹配命中的范围都以黄色标出。[0]表示每次匹配符合条件的字符串。[1]是第1个捕获组的内容,即属性名,[2]是第2个捕获组的内容,即属性值。

说明共进行了3次搜索,尽管每次搜索regExp.lastIndex都会改变,但每次的搜索范围都是被搜索字符串的全部。

共搜索了3次,这正是我们想要的效果。因为在每一次的匹配中,我们都可以提取出相应的属性值。这非常方便于语法加亮。

换用不同的模式,可能也可以匹配出某些结果,但由于greedy的原因,一些结果无法精确被提取出来。因此,编写模式时,一开始的思路很重要,然后再设法慢慢完善。

对于此例,在以后完善模式的过程中,应确保匹配3次,且每次均匹配出相应的属性名值组。

现在,将前面的"<div"也考虑进来。对于每个属性,它要么前面有"<div",要么什么都没有。

因此,模式改为:

(<div)?\s(\w+)='(\w*)'

匹配结果为:

Turn 1:
  [0]: <div width='20' height='30' color='red'>
  [1]: <div
  [2]: width
  [3]: 20

Turn 2:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: height
  [3]: 30

Turn 3:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: color
  [3]: red

现在,将"div"以括号括起来:

(<(div))?\s(\w+)='(\w*)'

匹配结果:

Turn 1:
  [0]: <div width='20' height='30' color='red'>
  [1]: <div
  [2]: div
  [3]: width
  [4]: 20

Turn 2:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: undefined
  [3]: height
  [4]: 30

Turn 3:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: undefined
  [3]: color
  [4]: red

[2]即为标签名称,[3]为属性名,[4]为属性值。

现在,将最后的">"包含进来。对于每个属性名值组来讲,它后面要么什么都没有,要么有一组0或多的空白符,以及一个">"。

(<(div))?\s(\w+)='(\w*)'(\s*>)?

匹配结果:

Turn 1:
  [0]: <div width='20' height='30' color='red'>
  [1]: <div
  [2]: div
  [3]: width
  [4]: 20
  [5]: undefined

Turn 2:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: undefined
  [3]: height
  [4]: 30
  [5]: undefined

Turn 3:
  [0]: <div width='20' height='30' color='red'>
  [1]: undefined
  [2]: undefined
  [3]: color
  [4]: red
  [5]: >

将模式中的"div"改为"\w+",以让它匹配任意标签名称。

(<(\w+))?\s(\w+)='(\w*)'(\s*>)?

String的replace方法

replace方法的模式中,可以带有以下特殊字符:

字符含义
$$ "$"本身
$& 所匹配的子字符串
$` 所匹配的子字符串之前的内容
$' 所匹配的子字符串之后的内容
$n 第n个括号内的内容。1 <= n < 100

Questioniar

如何匹配不要出现连续的几个字符? 例如,如果出现了"abc",即a与b与c连排在一起时,则匹配不成功。

Utilities

References