跳到主要内容

正则及零宽断言

1.正则表达式

声明正则表达式的俩种方式:字面量形式和new函数形式。

var reg = /test/,
newReg = new RegExp('test'); // 这俩种声明相同意思的正则不相等!
console.dir(reg);
console.dir(newReg);

new RegExp(reg)RegExp(reg) 的区别:(RegExp(reg)这个属于字面量的形式声明正则表达式)

var reg = /test/;
var newReg = RegExp(reg); // 不使用 new,此时 newReg 是 reg 的一个引用;使用 new 那么就是2个正则,俩者之间没有关系
console.log(reg === newReg);
reg.a = 1;
console.log(newReg.a);

1.1 字符类

匹配单个字符:

元字符描述元字符描述
.匹配单个字符,除了换行和行结束符\0匹配 NUL 字符
\w匹配单词字符\n匹配换行符
\W匹配非单词字符\f匹配换页符
\d匹配数字\r匹配回车符
\D匹配非数字\t匹配制表符
\s匹配空白符\v匹配垂直制表符
\S匹配非空白符\xxx匹配以八进制数 xxxx 规定的字符
\b匹配单词边界\xdd匹配以十六进制数 dd 规定的字符
\B匹配非单词边界\uxxxx匹配以十六进制 xxxx 规定的 Unicode 字符
?=n匹配任何其后紧接着指定字符串 n 的字符串?!n匹配任何其后紧接着不是指定字符串 n 的字符串
([^\d]*)匹配零个或多个非数字字符([^\w]*)匹配零个或多个非单词字符。([^\w] 表示非单词字符,包括空格、标点符号等)

1.2 量词

控制字符出现的次数:

限定符描述
n\*匹配包含任何包含 0 个或者多个 n 的字符串
n+匹配包含任何包含至少 1 个 n 的字符串
n?匹配包含 0 个或者 1 个 n 的字符串
n{x}匹配包含任何包含 x 个 n 的字符串
n{x,}匹配包含任何包含至少 x 个 n 的字符串
n{x,y}匹配包含任何包含至少 x 个,至 x 个 n 的字符串
n$匹配任何以 n 结尾的字符串
^n匹配任何以 n 开头的字符串

1.3 组和范围

一个中括号就代表一个字符,中括号的目的就是匹配某个特定字符。

分组说明
[abc]查找一个在中括号中的任意一个字符
[abc]查找一个不再中括号中的任意一个字符,^表示取反
[0-9]查找一个任何从 0 至 9 的数字
[a-z]查找一个任何从 a 至 z 的字母
[A-Z]查找一个任何从 A 至 Z 的字母
[A-z]查找一个任何从 A 至 z 的字母
[^A-Z]查找一个任何非 A 至 Z 的字母
[\u4e00-\u9fa5]查找一个汉字
n|m
/n正则表达式中与 n 括号匹配的最后一个子字符串的反向引用
(?<Name>x)具名捕获组'web-doc'.match(/-(?<name>\w)/).groups //{name: "d"}
(?:x)非捕获组: 匹配 “x”,但不记得匹配。

1.4 子表达式()、反向引用与修饰符

一个圆括号()代表一个子表达式,每一个子表达式都可以被反向引用引用。 反向引用:用来引用对应的子表达式。 修饰符:

  • g——global —— 全局匹配。
  • i——ignoreCase —— 忽略字母的大小写。
  • m——multiline —— 多行匹配
// 这里\1不是几次,而是反向引用第几个子表达式,一个()是一个子表达式,子表达式具有记忆功能,\w如果是a,那么反向引用还是a
var a = 'bbaaaaccaaaaddaaaayyy';
var reg = /(a)\1\1\1/g;
console.log(a.match(reg));

var a = 'bbaaaaccaaaaddaaaayyy';
var reg = /(\w)\1(\w)\2/g;
console.log(a.match(reg)); // ["bbaa", "aacc", "aaaa", "ddaa", "aayy"]

1.5 贪婪匹配和转义符

正则表达式默认是贪婪匹配(尽可能多的匹配),可以使用?来取消贪婪匹配,达到非贪婪匹配的目的。

var str = '<p>第一个标签</p><p>第二个标签</p>';
var str1 = str.match(/<p>(.+?)<\/p>/);
console.log('非贪婪模式:', str1);

在正则中表示特殊符号:去除.的特殊意义,需要转义: \.,一般的在[]中的不需要转义。

转义符号: \转义字符 \字符 \n \r \t

// 不同系统的的换行:
// windows: \r\n
// mac: \r
// liunx: \n

// 多行匹配,\n换行也代表字符串开头
var reg = new RegExp('^test', 'gim'); // reg = /^test/gim;
var str = 'This is a test.\nTest is important';
console.log(reg.test(str));
console.log(str.match(reg));

1.2 正则表达式的方法

1.2.1reg.exec(str)

一个在字符串中执行查找匹配的RegExp 方法,它返回一个数组(未匹配到则返回 null)。

var a = '123123123123';
// 注意这里是全局匹配,如果不加g,多次执行exec,依旧会匹配第一索引的内容
var reg = /123/g;
console.log(reg.exec(a), reg.lastIndex); // ["123", index: 0, input: "123123123123123", groups: undefined] 3
console.log(reg.exec(a), reg.lastIndex); // ["123", index: 3, input: "123123123123123", groups: undefined] 6
console.log(reg.exec(a), reg.lastIndex); // ["123", index: 6, input: "123123123123123", groups: undefined] 9
console.log(reg.exec(a), reg.lastIndex); // ["123", index: 9, input: "123123123123123", groups: undefined] 12
console.log(reg.exec(a), reg.lastIndex); // null 0

var a = 'aabbccccdddff';
var reg = /(\w)\1(\w)\2/g;
console.log(reg.exec(a)); // 此时会输出子表达式

1.2.2reg.text(str)

一个在字符串中测试是否匹配的RegExp 方法,它返回 true 或 false。

/^[a-z]{3}$/.test('abc'); // true

1.2.3str.macth(reg)

一个在字符串中执行查找匹配的String 方法,它返回一个数组,在未匹配到时会返回 null。

  • 如果使用 g 标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
  • 如果未使用 g 标志(str.match(reg)reg.exec(str) 相同),则仅返回第一个完整匹配及其相关的捕获组(Array)。 在这种情况下,返回的项目将具有如下所述的其他属性:
    • groups: 一个捕获组数组 或 undefined(如果没有定义命名捕获组)。
    • index: 匹配的结果的开始位置
    • input: 搜索的字符串.
var paragraph = 'aBcDeFgH';
console.log(paragraph.match(/[A-Z]/)); // ["B", index: 1, input: "aBcDeFgH", groups: undefined]
console.log(paragraph.match(/[A-Z]/g)); // ["B", "D", "F", "H"]

1.2.4str.macthAll(reg)

一个在字符串中执行查找所有匹配的String 方法,它返回一个迭代器(iterator)。

var regexp = /t(e)(st(\d?))/g;
var str = 'test1test2';
var array = [...str.matchAll(regexp)];
console.log(array); // [Array(4), Array(4)]

1.2.5str.search(reg)

一个在字符串中测试匹配的String 方法,它返回匹配到的位置索引,或者在失败时返回-1。

'abcdefg abc'.search(/c/); // 2

1.2.6str.replace(reg)

一个在字符串中执行查找匹配的String 方法,并且使用替换字符串替换掉匹配到的子字符串。

语法:str.replace(regexp|substr, newSubStr|function)

regexpsubstr的区别:

// regexp 和 substr 不具有全局匹配的能力,只会匹配第一个出现的字符,regexp可以加g实现全局匹配
'I like apple. And apple is fruit'.replace('apple', 'pear');
'I like apple. And apple is fruit'.replace(/apple/, 'pear');
'I like apple. And apple is fruit'.replace(/apple/g, 'pear');

newSubStr—替换字符串可以插入下面的特殊变量名:

变量名代表的值
$$插入一个 "$"。
$&插入匹配的子串。
`$``插入当前匹配的子串左边的内容。
$'插入当前匹配的子串右边的内容。
$n假如第一个参数是RegExp对象,并且 n 是个小于 100 的非负整数,那么插入第 n 个括号匹配的字符串。提示:索引是从 1 开始。如果不存在第 n 个分组,那么将会把匹配到到内容替换为字面量。比如不存在第 3 个分组,就会用“$3”替换匹配到的内容。
$<Name>这里Name是一个分组名称。如果在正则表达式中并不存在分组(或者没有匹配),这个变量将被处理为空字符串。只有在支持命名分组捕获的浏览器中才能使用。
// 将 a 替换为 $ 符号,第一个$转义,第二个$为字符
var str = '123a123a123',
reg = /a/g;
console.log(str.replace(reg, '$$')); // 123$123$123

// 将子串copy一份
var str = 'aabb1234ddff',
reg = /([\d]*)/g;
console.log(str.replace(reg, '$&$&')); // aabb12341234ddff

// 将匹配到的数字替换为其左边的字符串
var str = 'abc_112112f',
reg = /([0-9]{1,})/g;
console.log(str.replace(reg, '$`')); // abc_abc_f

// 将匹配到的数字替换为其右边的字符串
var str = 'abc_112112=fly=',
reg = /([0-9]{1,})/g;
console.log(str.replace(reg, "$'")); // abc_=fly==fly=

// 匹配分组 $n
var str = 'aabbccccddff',
reg = /(\w)\1(\w)\2/g;
console.log(str.replace(reg, '$2$2$1$1')); // bbaaccccffdd

function—指定一个函数作为参数:

下面是该函数的参数:

变量名代表的值
match匹配的子串。(对应于上述的$&。)
p1,p2, ...假如 replace()方法的第一个参数是一个RegExp 对象,则代表第 n 个括号匹配的字符串。(对应于上述的$1,$2 等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+p2 就是匹配的 \b+
offset匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 'abcd',匹配到的子字符串是 'bc',那么这个参数将会是 1)
string被匹配的原字符串。
NamedCaptureGroup命名捕获组匹配的对象
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, function (match, p1, p2, p3, offset, string) {
return '---';
});

这个正则方法是一个字符串的替换操作,它的作用是将匹配到的字符串替换成 '---',并返回替换后的结果。正则表达式的含义是:

  • ([^\d]*) 匹配零个或多个非数字字符([^\d] 表示非数字字符,* 表示匹配零个或多个)
  • (\d*) 匹配零个或多个数字字符(\d 表示数字字符)
  • ([^\w]*) 匹配零个或多个非单词字符([^\w] 表示非单词字符,包括空格、标点符号等)

这个正则表达式会将匹配到的内容分为三个组:

  • p1 表示第一个括号匹配的内容,即非数字字符
  • p2 表示第二个括号匹配的内容,即数字字符
  • p3 表示第三个括号匹配的内容,即非单词字符

在这个替换方法中,第二个参数是一个函数,该函数接受五个参数:match 表示匹配到的整个字符串,p1 表示第一个括号匹配的内容,p2 表示第二个括号匹配的内容,offset 表示匹配到的字符串在原始字符串中的偏移量,string 表示原始字符串。这个函数的返回值就是用来替换匹配到的字符串的新字符串,即 '---'。因此,这个正则表达式会将所有的非数字字符、数字字符、非单词字符都替换成 '---'。

1.2.7str.split(reg)

一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的String 方法

使用指定的分隔符字符串将一个 string 对象分割成子字符串数组,以一个指定的分割字串来决定每个拆分的位置。

/**
* str.split([separator[, limit]])
* separator 指定表示每个拆分应发生的点的字符串。
* limit 个整数,限定返回的分割片段数量。
*/
'123=abc=qwe=456'.split('='); // ["123", "abc", "qwe", "456"]
'123=abc=qwe=456'.split('=', 2); // ["123", "abc"]

2.零宽断言

在使用正则表达式时,有时候我们需要捕获的内容前后必须是特定的内容,但又不捕获这些特定的内容,零宽断言就起到作用。

零宽断言还有其他的名称,例如“环视”或者“预搜索”等等。

(?=exp) :零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式 exp—正向预查。

(?<=exp) :零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式 exp。

(?!exp) :零宽度负预测先行断言,断言此位置的后面不能匹配表达式 exp。

(?<!exp) :零宽度负回顾后发断言来断言此位置的前面不能匹配表达式 exp。

2.1 基本概念

零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。

作用是给指定位置添加一个限定条件,用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的字表达式匹配成功。

注意:这里所说的子表达式并非只有用小括号括起来的表达式,而是正则表达式中的任意匹配单元。

javascript 只支持零宽先行断言,而零宽先行断言又可以分为正向零宽先行断言,和负向零宽先行断言。

正向零宽先行断言——代码实例一如下:

var str = 'abZWab000abAAA863';
var reg = /ab(?=[A-Z])/;
console.log(str.match(reg)); // ["ab", index: 0, input: "abZWab000abAAA863", groups: undefined] —— 返回第一个符合条件的ab

在以上代码中,正则表达式的语义是:匹配后面跟随任意一个大写字母的字符串"ab"。最终匹配结果是"ab",因为零宽断言 (?=[A-Z]) 并不匹配任何字符,只是用来规定当前位置的后面必须是一个大写字母。

负向零宽先行断言——实例代码二如下:

var str = 'abZWab0000--863';
var reg = /ab(?![A-Z])/g;
console.log(str.match(reg)); // ["ab"]

在以上代码中,正则表达式的语义是:匹配"ab"子串,并且“ab”串之后的必须不能是大写字母。

2.2 匹配原理

上面代码只是用概念的方式介绍了零宽断言是如何匹配的。

下面就以匹配原理的方式分别介绍一下正向零宽断言和负向零宽断言是如何匹配的。

1.正向零宽断言:

var str = '<div>antzone';
var reg = /^(?=<)<[^>]+>/; // (?=<) 并不匹配任何字符,表示首位必须是<,(?=<)之后的<才表示第一个
console.log(str.match(reg)); // ["<div>antzone", index: 0, input: "<div>antzone", groups: undefined]

匹配过程如下:

首先由正则表达式中的"^"获取控制权,首先由位置 0 开始进行匹配,它匹配开始位置 0,匹配成功,然后控制权转交给"(?=<)",,由于"^"是零宽的,所以"(?=<)"也是从位置 0 处开始匹配,它要求所在的位置右侧必须是字符"<",位置 0 的右侧恰好是字符"<",匹配成功,然后控制权转交个"<",由于"(?=<)"也是零宽的,所以它也是从位置 0 处开始匹配,于是匹配成功,后面的匹配过程就不介绍了。

2.负向零宽断言:

var str = 'abZW863ab88';
var reg = /ab(?![A-Z])/; // (?![A-Z])匹配ab之后必须是非大写字母
console.log(str.match(reg)); // ["ab", index: 7, input: "abZW863ab88", groups: undefined]

匹配过程如下:

首先由正则表达式的字符"a"获取控制权,从位置 0 处开始匹配,匹配字符"a"成功,然后控制权转交给"b",从位置 1 处开始匹配,配字符"b"成功,然后控制权转交给"(?![A-Z])",它从位置 2 处开始匹配,它要求所在位置的右边不能够是任意一个大写字母,而位置的右边是大写字母"Z",匹配失败,然后控制权又重新交给字符"a",并从位置 1 处开始尝试,匹配失败,然后控制权再次交给字符"a",从位置 2 处开始尝试匹配,依然失败,如此往复尝试,直到从位置 7 处开始尝试匹配成功,然后将控制权转交给"b",然后从位置 8 处开始尝试匹配,匹配成功,然后再将控制权转交给"(?![A-Z])",它从位置 9 处开始尝试匹配,它规定它所在的位置右边不能够是大写字母,匹配成功,但是它并不会真正匹配字符,所以最终匹配结果是"ab"。

2.3 补充

零宽断言是正则表达式中的一种方法,正则表达式在计算机科学中,是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。

2.3.1.定义解释

零宽断言是正则表达式中的一种方法:

正则表达式在计算机科学中,是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。在很多文本编辑器或其他工具里,正则表达式通常被用来检索和/或替换那些符合某个模式的文本内容。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。正则表达式通常缩写成“regex”,单数有 regexp、regex,复数有 regexps、regexes、regexen。

2.3.2.零宽断言

用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧: 断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。

?=exp 也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式 exp。比如\b(?=re)\w+\b,匹配以 re 开头的单词的后面部分(除了 re 以外的部分),如查找 reading a book.时,它会匹配 ading。

var str = 'i am reading a book';
var reg = /\b(?=re)\w+\b/;
console.log(str.match(reg));

?<=exp 也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式 exp。比如\b\w+(?<=ing\b)会匹配以 ing 结尾的单词的前半部分(除了 ing 以外的部分),例如在查找 I am reading a book.时,它匹配 read。

var str = 'i am reading a book';
var reg = /\b\w+(?<=ing\b)/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg));

3.3.负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?

例如,如果我们想查找这样的单词--它里面出现了字母 q,但是 q 后面跟的不是字母 u,我们可以尝试这样:

var str = 'queue aqi';
var reg = /\b\w*q[^u]\w*\b/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg));

\b\w*q[^u]\w*\b 匹配包含后面不是字母 u 的字母 q 的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果 q 出现在单词的结尾的话,像 Iraq,Benq,这个表达式就会出错。

var str = 'queue aiq qww ad';
var reg = /\b\w*q[^u]\w*\b/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg)); // aiq qww

这是因为 [^\u] 总要匹配一个字符,所以如果 q 是单词的最后一个字符的话,后面的 [^u] 将会匹配 q 后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的 \w*\b 将会匹配下一个单词,于是 \b\w*q[^u]\w*\b 就能匹配整个 Iraq fighting。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题: \b\w*q(?!u)\w*\b

零宽度负预测先行断言 ?!exp ,断言此位置的后面不能匹配表达式 exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b 匹配不包含连续字符串 abc 的单词。

var str = '12ab23 999l';
var reg = /\d{3}(?!\d)/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg));
var str = 'abc123 qwe123';
var reg = /\b((?!abc)\w)+\b/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg));

同理,我们可以用 ?<!exp ,零宽度负回顾后发断言来断言此位置的前面不能匹配表达式 exp:(?<![a-z])\d{7} 匹配前面不是小写字母的七位数字。

var str = 'a1234567 A7654321';
var reg = /(?<![a-z])\d{7}/;
console.log(str.match(reg));

// 升级
var str = 'a1234567 A7654321';
var reg = /\b(?<![a-z])[A-Z]\d{7}\b/;
console.log(str.match(reg));

一个更复杂的例子:(?<=<(\w+)>).*(?=<\/\1>) 匹配不包含属性的简单 HTML 标签内里的内容。(<?=(\w+)>) 指定了这样的前缀:被尖括号括起来的单词(比如可能是<b>),然后是 .* (任意的字符串),最后是一个后缀 (?=<\/\1>)。注意后缀里的\/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是<b>的话,后缀就是</b>了。整个表达式匹配的是<b></b>之间的内容(再次提醒,不包括前缀和后缀本身)。

var str = '<h1>哈哈哈</h1>';
var reg = /(?<=<(\w+)>).*(?=<\/\1>)/; // 俩个\b分别是表示单词的左右边界
console.log(str.match(reg));

上面的看了有点伤脑筋啊。下面来点补充:

(?=exp) 也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式 exp。

var str = "I'm singing while you're dancing.";
var reg = /\b(\w+(?=ing\b))/g;
console.log(str.match(reg)); // ["sing", "danc"]

(?<=exp) 也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式 exp。

var str = 'reading a book';
var reg = /(?<=\bre)\w+\b/g;
console.log(str.match(reg)); // ["ading"]