正则表达式的模式匹配

前端开发
2018年10月29日
808

正则表达式(regular expression)是一个描述字符模式的对象。JavaScript的RegExp类表示正则表达式,StringRegExp都定义了方法,后者使用正则表达式进行强大的模式匹配主文本检索与替换功能。JavaScript的正则表达式语法是Perl5的正则表达式语法的大型子集。

注意,有一些Perl正则表达式语法特性并不被ECMAScript支持。这些特性包括:s(单行模式)和x(扩展语法)标记;\a\e\l\u\L\U\E\Q\A\Z\z\G转义字符;(?<=正向后行断言和(?<!负向后行断言;(?#注释和(?的语法。

正则表达式的定义

JavaScript中的正则表达式用RegExp对象表示,可以使用RegExp构造函数来创建。不过RegExp对象 更多的是通过一种特殊的直接量语法来创建。就像通过引号包裹字符的方式来定义字符串直接量一样,正则表达式直接量定义为包含在一对斜杠//之间的字符。比如:

js
var pattern = /s$/;

上面的代码表示创建一个新的RegExp对象,并将它赋值给pattern变量。这个特殊的RegExp对象用来匹配所有以字母s结尾的字符串。用构造函数也可以定义与之等价的正则表达式。

js
var pattern = new RegExp('s$');

正则表达式的模式规则是由一个字符序列组成的。包括所有字母和数字在内,大多数的字符都是按照直接量仅描述待匹配的字符中。如此说来,正则表达式/java/可以匹配任何包含java子串的字符串。除此之外,正则表达式中还有其他具有特殊语义的字符,这些字符并不按照字面含义进行匹配。比如,正则表达式/s$/包含两个字符,第一个字符s按照字面含义匹配,第二个字符$是一个具有特殊语义的元字符,用以匹配字符串的结束。因此这个正则表达式可以匹配任何以s结束的字符串。

直接量

所谓直接量(literal),就是程序中直接使用的数据值。下面列出的都是直接量:

js
12 // 数字 1.2 // 浮点数 'hello' // 字符串广西 true // 布尔值 /javascript/gi // 正则表达式直接量(用作模式匹配) null // 空

更多复杂的表达方式可以写成数组或对象直接量:

js
{ x: 1, y: 2 } // 对象 [1, 2, 3] // 数组

RegExp直接量和对象的创建

就像字符串和数字一样,程序中的每个取值相同的原始类型直接量表示相同的值,这是显而易见的。程序运行时每次遇到对象直接量(初始化表达式)诸如{}[]的时候都会创建新对象。比如,在循环体中写var a = [];,则每次遍历都会创建一个新的空数组。

正则表达式直接量则与此不同,ECMAScript3规范规定,一个正则表达式直接量会在执行到它时转换为一个RegExp对象,同一段代码所表示的正则表达式直接量的每一次运算都会返回同一个对象。ECMAScript5规范则做了相反的规定,同一段代码所表示的正则表达式直接量的每次运算都返回新对象。IE一直都是按照ECMAScript5规范实现的,多数最新版本的浏览器也开始遵循ECMAScript5,尽管目前该标准还未全面广泛推行。

直接量字符

如上文提到的,正则表达式中的所有字母和数字都是按照字面含义进行匹配的,JavaScript正则表达式语法也支持非字母的字符匹配,这些字符需要通过反斜线\来转义。比如,\n用于匹配换行符。

字符 匹配
字母和数字字符 自身
\o NUL字符(\u0000)
\t 制表符(\u0009)
\n 换行符(\u000A)
\v 垂直制表符(\u000B)
\f 换页符(\u000C)
\r 回车符(\u000D)
\xnn 由十六进制数nn指定的拉丁字符,比如:\x0A等价于\n
\uxxxx 由十六进制数xxxx指定的Unicode字符,比如:\u0009等价于\t
\cX 控制字符^X,比如:\cJ等价于换行符\n

在正则表达式中,许多标号符号具有特殊含义,它们是:

plain-text
^ $ . * + ? = ! : | / \ () {} []

某些符号只有在正则表达式的某些上下文中才具有某种特殊含义,在其他上下文中则被当成直接量处理。然而,如果想在正则表达式中使用这些字符的直接量直接匹配,则需要加上\来转义,这是一条通行的规则。其他标点符号(比如@和引号)没有特殊含义,在正则表达式中按照字面的含义进行匹配。

如果不记得哪些标点符号需要\转义,可以在每个标点符号前面加上\。值得注意的是,许多字母和数字在有\作为前缀的时候也有特殊含义,所以对于想按照直接量进行匹配的字母和数字,尽量不要使用\。当然,要想在正则表达式中按照直接量匹配\本身,则必须使用\将其转义。比如,正则表达式/\\/用于匹配任何包含反斜杠的字符串。

字符类

将直接量字符单独放进方括号内就组成了字符类(character class)

一个字符类可以匹配它所包含的任意字符

js
/[abc]/ // 匹配"a"、"b"、"c"中的任意一个

通过^符号来定义字符类,它匹配所有不包含在方括号内的字符。定义否定字符类时,将^作为括号仙的第一个字符。

js
/[^abc]/ // 匹配"a"、"b"、"c"之外的任意字符。

字符类可以使用连字符来表示字符范围。

js
/[a-z]/ // 匹配拉丁字母表中的小写字母 /[a-zA-Z0-9]/ // 匹配拉丁字母表中的任何字母和数字
字符 匹配
[…] 方括号内的任意字符
[^…] 不在方括号字符内的任意字符
. 除换行符和其他Unicode行终止符之外的任意字符
\w 任何ASCII字符组成的单词,等价于[a-zA-Z0-9]
\W 与\w相反,等价于[^a-zA-Z0-9]
\s 任何Unicode空白符
\S 任何非Unicode空白符的字符,注意\w和\S不同
\d 任何ASCII数字,等价于[0-9]
\D 除ASCII数字之外的任意字符,等价[^0-9]
[\b] 退格直接量(特例)

注意,在方括号之内也可以写这些特殊转义字符。比如,由于\s匹配所有的空白字符,\d匹配的是所有数字,因此/[\s\d]/就匹配任意空白符或者数字。注意,这里有一个特例,[\b],当用在字符类中时,它表示的是退格符,所以要在正则表达式中按照直接量表示一个退格符,只需要使用具有一个元素的字符类[\b]

重复

到目前为止,还没有一种方法可以用来描述任意多位的数字,或者描述由三个字母和一个数字构成的字符串。这些正则表达式语法中转为复杂的模式都提到了正则表达式中某元素的“重复出现次数”。

我们在正则模式之后跟随用以指定字符重复的标记。由于某些重复各类非常常用,因此就有一些专门用于表示这种情况的特殊字符。比如,+用于匹配一个模式的一个或多个副本。

字符 含义
{n,m} 匹配前一项至少n次,但不能超过m次
{n,} 匹配前一项n次或者更多次
{n} 匹配前一项n次
? 匹配前一项0次或1次,也就是说前一项是可选的,等同于{0,1}
+ 匹配前一项1次或多次,等同于{1,}
* 匹配前一项0次或多次,等同于{0,}
js
/\d{2,4}/ // 匹配2-4个数字 /\w{3}\d?/ // 精确匹配三个单词和一个可选的数字 /\s+java\s+/ // 匹配前后带有一个或多个空格的字符串"java" /[^(]*/ // 匹配一个或多个非左括号的字符

在使用*?时需要注意,由于这些字符可能匹配0个字符,因此它们允许什么都不匹配。比如,正则表达式/a*/实际上可以与bbbb匹配,因为这个字符串含有0个a

非贪婪的重复

上面列出的匹配重复字符是尽可能多地匹配,而且允许后续的正则表达式继续匹配。因此,我们称之为“贪婪的”匹配。我们两样可以使用正则表达式进行非贪婪匹配,只需要在待匹配字符后跟随一个问号即可:??+?*?{1,5}?。比如,正则表达式/a+/可以匹配一个或多个连续的字母a,当使用aaa作为匹配字符串时,正则表达式会匹配它的三个字符。/a+?/也可以匹配一个或多个连续的字母a,但它是尽可能少地匹配,使用aaa作为匹配字符串时,它只能匹配第一个a。

使用非贪婪的匹配模式所得到 的结果有可能和期望的并不一致。

/a+b/,它可以匹配一个或多个a,以及一个b。当使用aaab作为匹配字符串时,它会匹配整个字符串。当使用/a+?b/来匹配aaab时,你期望它能得到最后一个ab,但实际上,这个模式却匹配了整个字符串,和贪婪模式/a+b/一样。

这是因为正则表达式的模式匹配总是会寻找字符串中第一个可能匹配的位置,由于 该匹配是从字符串的第一个字符开始的,因此在这里不考虑它的子串中还有更短的匹配。

选择、分组和引用

正则表达式的语法还包括指定选择项、子表达式分组和引用前一个子表达式的特殊字符。字符|用于分隔供选择的字符。例如,/ab|cd|ef/可以匹配字符串ab,也可以匹配cd,还可以匹配ef/\d{3}|[a-z]{4}/匹配的是三个数字或者四个小写字母。

注意,选择项的尝试匹配次序是从左到右,直接发现了匹配顶。

js
/a|ab/ // 匹配字符串 "ab"时,只能匹配到 "a"

正则表达式中的圆括号()有多种作用。一个作用是把单独的项组合成子表达式,以便可以像处理一个独立的单元那样用|*+或者?等来对单元内的项进行处理。比如,/java(script)?/可以匹配java,其后可以有script,也可以没有。/(ab|cd)+|ef/可以匹配ef,也可以匹配abcd的一次或多次重复。

圆括号的另一个作用是在完事的模式匹配中定义子模式。当一个正则表达式成功地和目标字符串相匹配时,可以从目标串中抽出和圆括号中的子模式相匹配的部分。比如,假定我们正在检索的模式是一个或多个小写字母后面跟随一位或多位数字,则可以使用/[a-z]+\d+/。但如果我们真正关心的是每个匹配尾部的数字,则可以使用/[a-z]+(\d+)/,就可以检索到匹配中抽取数字了。

带圆括号的表达式的另一个用途是允许在同一个正则表达式的后部引用前面的子表达式。这是通过在字符\后加一位或多位数字来实现。这个数字指定了带圆括号的子表达式在正则表达式中的位置。比如,\1引用的是第一个带圆括号的子表达式,\3表达的是第3个带圆括号的子表达式。注意,因为子表达式可以嵌套另一个子表达式,所以它的位置是参与计数的左括号(的位置。比如,在下面的正则表达式中,嵌套的子表达式([Ss]cript)可以用\2来指代。

js
/([Jj]ava(Ss)cript?)\sis\s(fun\w*)/

对正则表达式中前一个子表达式的引用,并不是指对子表达式模式的引用,而指的是那个模式相匹配的文本的引用。这样,引用可以用于实施一条约束,即一个字符串或各个单独部分包含的是完全相同的字符。但是,它前不要求左侧和右侧的引用匹配(即加入的两个引号都是单引号或都是双引号):

js
/['"][^'"]*['"]/

如果要匹配左侧或右侧的引号,可以使用如下的引用:

js
/['"][^'"]*\1/

\1匹配的是第一个带圆括号的子表达式所匹配的模式。在这个例子中,存在这样一条约束,那就是左侧的引号必须和右侧的引号相匹配。正则表达式不允许用双引号括号的内容中有单引号,反之亦然。

js
// 下面这种写法是非法的 /(['"])[^\1]*\[\1]/

同样,在正则表达式中不用创建带数字编码的引用,也可以对子表达式进行分组。它不是以“(”和“)”进行分组,而是以“(?:”和“)”来进行分组。

js
/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/

上面的代码中,子表达式(?:[Ss]cript)仅仅用于分组,因此复制符号?可以应用到各个分组。这种改进的圆括号并不生成引用,所以在这个正则表达式中,\2引用了与(fun\w*)匹配的文本。

字符 含义
| 选择,匹配的是该符号左边的子表达式或右边的子表达式
(…) 组合,将几个项组合成一个单元,这个单元可通过“*”、“+”、“?”和“|”等符号加以修饰,而且可以记住和这个组合 相匹配的字符串以供此后的引用使用
(?:…) 只组合,不记忆
\n 和第n个分组第一次匹配的字符相匹配

指定匹配位置

字符 含义
^ 匹配字符串的开头,在多行检索中,匹配一行的开头
$ 匹配字符串的结尾
\b 匹配一个单词的边界,简言之,就是位于字符\w和\W之间的位置,或位于字符\w和字符串开头或结尾之间的位置(但需要注意,[\b]匹配的是退格符)
\B 匹配非单词边界的位置
(?=p) 零宽正向先行断言,要示接下来的字符都与p匹配,但不能包括匹配p的那些字符
(?!p) 零宽负向先行断言,要求接下来的字符p匹配

修饰符

正则表达式的修饰符,用于说明高级匹配模式的规则。和之前讨论的正则表达式语法不同,修饰符是放在/符号之外的,也就是说,它们不是出现在//之间,而是在之后。

JavaScript支持3个修饰符:

字符 含义
i 执行不区分大小写的匹配
g 执行一个全局匹配,即找到所有匹配,而不是在找到第一个之后就停止
m 多行匹配

用于模式匹配的String方法

String支持4种使用正则表达式的方法:

  1. search():参数是一个正则表达式,返回第一个与之匹配的子串的起始位置,如果找不到,将返回-1。

    js
    "JavaScript".search(/srcipt/i);

    如果search方法的参数不是正则表达式,则会先通过RegExp构造函数将它转为正则表达式,search方法不支持全局检索,因为它忽略正则表达式参数中的修饰符g

  2. replace():第一个参数是正则表达式,第二个参数是要进行替换的字符串。这个方法会调用它的字符串进行检索,使用指定的模式来匹配。如果正则表达式中设置了修饰符g,那么源字符串中所有与模式匹配的子串都将替换成第二个参数指定的字符串;如果不带修饰符g,则替换第一个与之相匹配的子串。

    js
    var text = '将所有不区分大小写的javascript都替换成大小写正确的JavaScript'; text.replace(/javascript/gi, 'JavaScript');

    如果在替换字符串中出现了$加数字,那么replace方法将用与指定的子表达式相匹配的文本来替换这两个字符。

    js
    // 一段引用文本起始于引号,结束于引号 // 中间的内容区域不能包含引号 let quote = /"([^"]*)"/g; // 用中文半角引号替换英文引号,同时要保持引号之间的内容(存储在$1中)没有被修改 text.replace(quote, '“$1”');

    值得注意的是,replace方法的第二个参数也可以是一个函数,该函数能够动态地计算替换字符串。

  3. match():参数是一个正则表达式,返回一个由匹配结果组成的数组。

    js
    "1 plus 2 equals 3".match(/\d+/g) // 返回 ["1", "2", "3"]

    如果没有修饰符gmatct方法就不会进行全局检索,但是返回的依然是一个数组。在这种情况下,数组的第一个元素就是匹配的字符串,余下的元素则是正则表达式中用圆括号括起来的子表达式。

    js
    let url = /(\w+):\/\/([\w.]+)\/(\S*)/; let text = 'Visit http://www.example.com/~david'; let result = text.match(url); if (result) { var fullurl = result[0]; // 'http://www.example.com/~david' var protocol = result[1]; // 'http' var host = result[2]; // 'www.example.com' var path = result[3]; // '~david' }

    值得注意的是,给match方法传入一个非全局的正则表达式,实际上和给这个正则表达式的exec()传入的字符串一模一样,它返回的数组带两个属性:indexinput

  4. split():将字符串一个子串组成的数组,使用的分隔符则是split()的参数。

    js
    "123,456,789".split(',') // ['123', '456', '789']

    split方法的参数也可以是一个正则表达式,这使用split方法异常强大。

    js
    // 指定分隔符,允许两边存在任意多的空白符 '1, 2, 3 , 4, 5'.split(/\s*,\s*/) // ['1', '2', '3', '4', '5']

RegExp对象

RegExp构造函数用于创建新的RegExp对象,可以有两个字符串参数,其中第二个参数是可选。

js
// 全局匹配字符串中的5个数字,注意这里使用 “\\” 而不是 “\” var zipcode = new RegExp('\\d{5}', 'g');

RegExp的属性

每个RegExp对象都包含5个属性。

  • source:(只读),包含正则表达式的文本。
  • global:(只读,布尔值),表示这个正则表达式是否带有修饰符g
  • ignoreCase:(只读,布尔值),表示这个正则表达式是否带有修饰符i
  • multiline:(只读,布尔值),表示这个正则表达式是否带有修饰符m
  • lastIndex:(整数),如果匹配模式带有修饰符g,这个属性存储在整个字符串中下一次检索的开始位置,这个属性会被exec()test()用到。

RegExp的方法

RegExp对象定义了两个用于执行模式匹配操作的方法:

  1. exec():对一个指定的字符串执行一个正则表达式,也就是在一个字符串执行匹配检索。如果没有找到任何匹配,返回null,反之返回一个数组,这个数组的第一个元素包含的是与正则表达式相匹配的字符串,余下的元素是与圆括号内的子表达式想匹配的子串。属性index包含了发生匹配的字符的位置,属性input引用的是正在检索的字符串。和match方法不同,不管正则表达芝是否具有全局修饰符gexec方法都会返回一样的数组。当调用exec()的正则表达式对象具有修饰符g时,它将把当前正则表达式对象的lastIndex属性所指示的字符处开始检索。如果exec()没有发现任何匹配结果,它会将lastIndex重置为0这种特殊的行为使我们可以在用正则表达式匹配字符串的过程中反复调用exec()

    js
    var pattern = /Java/g; var text = 'JavaScript is more fun than Java!'; var result; while ((result = pattern.exec(text)) != null) { alert(`Matched '${result[0]}' at position ${result.index};next search begins at ${pattern.lastIndex}`); }
  2. test():它的参数是一个字符串,用test()对某个字符串进行检测,如果包含正则表达式匹配的结果,则返回true

    js
    var pattern = /java/i pattern.text('JavaScript'); // true