正则表达式从入门到入坑

入坑前先介绍两个辅助网站:
正则表达式测试网站:​​​https://regex101.com​​​ 正则表达式思维导图:​​https://regexper.com​


正则基础(入门)

1、元字符

进入正题,我们先去了解最基本的字符及其初步应用。

元字符

描述

\

将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。

^

匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。

$

匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。

\b

匹配一个单词边界,也就是指单词和空格间的位置。

\B

匹配非单词边界。

\d

匹配一个数字字符。等价于 [0-9]。

\D

匹配一个非数字字符。等价于 [^0-9]。

\f

匹配一个换页符。

\n

匹配一个换行符。

\r

匹配一个回车符。

\s

匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。

\S

匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。

\w

匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。

\W

匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'。

不运用起来的知识都不是自己的知识,所以看完总得写点例子建立思维记忆。
比如:
1、匹配有hello开头的字符串:

let str = "hello world";
// 方法一
let reg = /^hello/;
reg.test(str); //true
// 方法二
let reg2 = /\bhello/;
reg2.test(str); //true

这么一看\b和^好像功能差不多,其实不然,我们看下一个例子:

let str = "say hello";
let reg = /^hello/;
reg.test(str); //false
let reg2 = /\bhello/;
reg2.test(str); //true

可以看出\b并不是匹配开头,它匹配的是单词边界。

2、匹配1开头的11位数字的手机号码:

let phone = "13388882983";
let reg = /^1\d\d\d\d\d\d\d\d\d\d$/

3、匹配8的数字、字母和下划线组成的密码:

let password = "A_1234_b"
let reg = /^\w\w\w\w\w\w\w\w$/

2、重复限定符

匹配每一个数字都得写一个/d,代码怎么可以这么冗余,我们追求的是优雅,那该怎么写呢?我们先看下面的限定符。

语法

描述

*

匹配前面的子表达式零次或多次。* 等价于{0,}。

+

匹配前面的子表达式一次或多次。+ 等价于 {1,}。

?

匹配前面的子表达式零次或一次。? 等价于 {0,1}。

{n}

n 是一个非负整数。匹配确定的 n 次。

{n,}

n 是一个非负整数。至少匹配n 次。

{n,m}

m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。配n 次。

看完我们对刚刚的正则进行一点点优雅的改造。
1、匹配8的数字、字母和下划线组成的密码:

let password = "A_1234_b"
let reg = /^\w{8}$/

但是产品觉得限制8位太不灵活了,它要8-15位,好,满足它:

let password = "A_1234_b"
let reg = /^\w{8,15}$/

2、匹配以a开头的,中间带有一个或多个b,0个或多个c结尾的字符串:

let reg = /^ab+c*$/;
let str = "abbc";
reg.test(str); //true
let str2 = "abb";
reg.test(str2); //true
let str3 = "acc";
reg.test(str3); //false

3、区间 []

产品想法越来越多,密码希望只能给用户设置由大小写字母和数字组成的8-15位密码,摸了摸刀柄,决定继续满足它。

let reg = /^[A-Za-z0-9]{8,15}$/;
let password = "A123456789b";
reg.test(password); //true
let password2 = "A_1234_b";
reg.test(password2); //false

4、条件或

产品给你点了个赞然后提出了手机号码验证要优化的想法,调查发现VIP客户的手机只有13、156、176、186开头的11位数,要我们进行精确一点匹配。看了一眼它更长的刀,默默的写下下面的正则:

let reg = /^(13\d|156|176|186)\d{8}$/

产品表示很满意,结束了它的基本需求。

5、修饰符

标记也称为修饰符,正则表达式的标记用于指定额外的匹配策略。
标记不写在正则表达式里,标记位于表达式之外,格式如下:

/pattern/flags

修饰符

含义

描述

i

ignore - 不区分大小写

将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。

g

global - 全局匹配

查找所有的匹配项。

m

multiline - 多行匹配

使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。

s

特殊字符圆点 . 中包含换行符 \n

默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the/
str.match(reg); //["the",index:16]

通常正则匹配到第一个就会自动结束,因此我们只能匹配到the fish中的ths就结束了,如果我们希望把后面的"the"也匹配出来呢?这时候我们就要加一个全局匹配修饰符了。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the/g
str.match(reg); //["the","the"]

要是希望把开头大写的"The"也一起匹配出来呢?这时候我们需要再加多一个全局匹配修饰符i。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the/gi
str.match(reg); //["The","the","the"]

一般我们使用^或$只会匹配文章的开头和结尾。

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /^The/g
str.match(reg); //["The"]

但是如果我们需要匹配各个段落的开头呢?

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /^The/gm
str.match(reg); //["The","The"]

默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符。如:

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /.+/
str.match(reg); //["The fat cat eat the fish on the dish.",...]

我们发现遇到\n的时候会切换了匹配,如果我们想继续完全匹配下去,需要加上修饰符s。

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /.+/s
str.match(reg); //['The fat cat eat the fish on the dish.\nThe cat is beautiful.',...]

6、运算符优先级

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。
相同优先级的从左到右进行运算,不同优先级的运算先高后低。下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

运算符

描述

\

转义符

(), (?: ), (?=), []

圆括号和方括号

*, +, ?, {n}, {n,}, {n,m}

限定符

^, $, \任何元字符、任何字符

定位点和序列(即:位置和顺序)

|

替换,"或"操作

字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。


Js正则常用方法

1、定义正则

定义正则有下面两种方式:

// 第一种
//RegExp对象。参数就是我们想要制定的规则。
let reg = new RegExp("a");
// 第二种
// 简写方法 推荐使用 书写简便、性能更好。
let reg = /a/;

2、test()

在字符串中查找符合正则的内容,若查找到返回true,反之返回false。
例:

let reg = /^ab+c*$/;
let str = "abbc";
reg.test(str); //true

3、match()

在字符串中搜索复合规则的内容,搜索成功就返回内容,格式为数组,失败就返回null。
例:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/;
str.match(reg); //["the",index:16,...]

全局匹配匹配到多个是则在数组中按序返回,如:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/g;
str.match(reg); //["the","the"]

4、search()

在字符串搜索符合正则的内容,搜索到就返回坐标(从0开始,如果匹配的不只是一个字母,那只会返回第一个字母的位置), 如果搜索失败就返回 -1 。
例:

let str = 'abc';
let reg = /bc/;
str.search(reg); //1

5、exec()

和match方法一样,搜索符合规则的内容,并返回内容,格式为数组。

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/;
reg.exec(str); //["the",index:16]

如果是全局匹配,可以通过while循环 找到每次匹配到的信息。如:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/g;
let res = "";
while(res = reg.exec(str)){
console.log(res);
}
/**
* 匹配到两次
* 第一次:
* [
0: "the"
groups: undefined
index: 16
input: "The fat cat eat the fish on the dish."
* ]
*
* 第二次:
* [
0: "the"
groups: undefined
index: 28
input: "The fat cat eat the fish on the dish."
* ]
*/

6、replace()

查找符合正则的字符串,就替换成对应的字符串。返回替换后的内容。
replace方法接收两个参数,第一个是正则,第二个是替换字符/回调方法,我们下面分别举例说明:
例1:

let str = 'abc';
let reg = /a/;
str.replace(reg,"A"); //"Abc"

例2:

let str = 'abc';
let reg = /a/;
str.replace(reg,function(res){
console.log(res); //'a'
return "A"; //不return则会返回undefine,输出结果则会变成"undefinedbc"。
}); //"Abc"

除此以外replace还有更深入的用法,会放在后面入坑那里再说。


正则进阶(入坑)

1、零宽断言

我们先去理解零宽和断言分别是什么。
--零宽:就是没有宽度,在正则中,断言只是匹配位置,不占字符,也就是说,匹配结果里是不会返回断言本身。
--断言:俗话的断言就是“我断定什么什么”,而正则中的断言,就是说正则可以指明在指定的内容的前面或后面会出现满足指定规则的内容。
总结:
零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已。
javascript只支持零宽先行断言,而零宽先行断言又可以分为正向零宽先行断言,和负向零宽先行断言。

1、 正向先行断言(正向肯定预查):
--语法:(?=pattern)
--作用:匹配pattern表达式的前面内容,不返回本身。
我们来举个栗子:

The fat cat eat the fish on the dish.

我们希望拿到fat前面的字符串The。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the(?=\sfat)/gi
str.match(reg); //["The"]

2、负向先行断言(正向否定预查):
--语法:(?!pattern)
--作用:匹配pattern表达式的前面内容,不返回本身。
那如果我们希望拿到不是fat前面的字符串The呢?很简单:

let str = 'The fat cat eat the fish on the dish.'
let reg = /the(?!\sfat)/gi
str.match(reg); //["the","the"]

3、正向后行断言(反向肯定预查):
--语法:(?<=pattern)
--作用:匹配pattern表达式的后面的内容,不返回本身。
继续举个栗子:

This is the flower cat and the civet cat.

我们希望拿到flower后面的cat。

let str = `This is the flower cat and the civet cat.`
let reg = /(?<=flower\s)cat/
str.match(reg); //["cat",index:19]

4、 负向后行断言(反向否定预查)
--语法:(?<!pattern)
--作用:匹配非pattern表达式的后面内容,不返回本身。
那如果我们希望拿到不是flower后面的cat呢?

let str = `This is the flower cat and the civet cat.`
let reg = /(?<!flower\s)cat/
str.match(reg); //["cat",index:37]

2、捕获和非捕获

单纯说到捕获,他的意思是匹配表达式,但捕获通常和分组联系在一起,也就是“捕获组”。

捕获组:匹配子表达式的内容,把匹配结果保存到内存中中数字编号或显示命名的组里,以深度优先进行编号,之后可以通过序号或名称来使用这些匹配结果。

而根据命名方式的不同,又可以分为两种组:

1、数字编号捕获组:
语法:(exp)
解释:从表达式左侧开始,每出现一个左括号和它对应的右括号之间的内容为一个分组,在分组中,第0组为整个表达式,第一组开始为分组。
举个例子:

let phone = "020-85653333";
let reg = /(0\d{2})-(\d{8})/;
phone.match(reg);
//输出结果:
[
0: "020-85653333",
1: "020",
2: "85653333",
groups: undefined,
index: 0,
input: "020-85653333"
]

其实分组个数是2,但是因为第0个为整个表达式本身,因此也一起输出了。

2、 命名编号捕获组:
语法:(?exp)
解释:分组的命名由表达式中的name指定。
比如我们电话匹配加上命名:

let phone = "020-85653333";
let reg = /(?<quhao>0\d{2})-(?<num>\d{8})/;
phone.match(reg);
//输出结果:
[
0: "020-85653333",
1: "020",
2: "85653333",
groups: {quhao: "020", num: "85653333"},
index: 0,
input: "020-85653333"
]

输出结构可以看到,groups对象会以命名分组存放对应的匹配数据。

3、非捕获组:
语法:(?:exp)
解释:和捕获组刚好相反,它用来标识那些不需要捕获的分组,说的通俗一点,就是你可以根据需要去保存你的分组。
如果我们不想匹配区号,那我们可以:

let phone = "020-85653333";
let reg = /(?:0\d{2})-(\d{8})/;
phone.match(reg);
//输出结果:
[
0: "020-85653333",
1: "85653333",
groups: undefined,
index: 0,
input: "020-85653333"
]

3、反向作用

捕获会返回一个捕获组,这个分组是保存在内存中,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。
根据捕获组的命名规则,反向引用可分为:
1、数字编号组反向引用:\number
2、命名编号组反向引用:\k<name>
概念都是比较模糊,我们直接举例说明:
我们有串字符'aabbcddddeffg',需要捕获两个连续相同的字母。我们需要解决的关键在于怎么判断上下两个字母是相同。

let str = 'aabbcddddeffg';
let reg = /(\w)\1/g;
str.match(reg); // ["aa", "bb", "dd", "dd", "ff"]

这其中的\1是什么意思呢?其实就是获取捕获的第一个分组,下面我们再举例说明

let reg = /(\d)(\d)\d\1\2/;
let str = 'a12312b';
reg.test(str); //true
// 第一个(\d)捕获匹配到了1,这时候会存在内存,\1=1,
// 第二个(\d)捕获匹配到了2,\2=2,
// 此时正则表达式的可以解读成/12\d12/
let str1 = 'a12345b';
reg.test(str1); //false

4、贪婪和非贪婪

1、贪婪匹配:
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符,这匹配方式叫做贪婪匹配。
贪婪匹配是重复限定符( *, +, ?, {n}, {n,}, {n,m} )特有的。
举个例子:

let phone = "aibicidiei";
let reg = /a\w*i/g;
phone.match(reg); //["aibicidiei"]

"ai"其实已经可以满足匹配规则,但是在贪婪模式上它并不满足,而是匹配到了最大能匹配的字符"aibicidiei"。

2、懒惰(非贪婪):
有贪婪模式那必然也有非贪婪模式。
特性:当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能少的字符,这匹配方式叫做懒惰匹配。
懒惰量词是在贪婪量词后面加个?,如:

let phone = "aibicidiei";
let reg = /a\w*?i/g;
phone.match(reg); //["ai"]

5、反义

语法:[^]
用得不多,就简单提及一下,如不想匹配abc这三个字符:

let reg = /[^abc]/

6、replace

上文又提及过replace第二个参数可以是字符串或者函数。
字符串的时候,它有几个特定字符。

字符

描述

$

匹配字符串左边的字符

$'

匹配字符串右边的字符

$&

与正则相匹配的字符串

$i (i:1-99)

匹配结果中对应的分组匹配结果

看着有点抽象,写个代码就一目了然了。

'abc'.replace(/b/,"$");     //acc
'abc'.replace(/b/,"$`"); //aac
let str = '正则表达式从入门到入坑';
str.replace(/正则表达式/,'{$&}'); //"{正则表达式}从入门到入坑"
let str2 = 'xyz';
str2.replace(/(x)(y)(z)/,"$3$2$1"); //"zyx"

第二个参数是函数,且正则使用分组捕获的时,函数会3个参数分别是:
0、匹配到的子字符串;
1、匹配到的子串的索引位置;
2、源字符串本身;

let str = "This is the flower cat and the civet cat.";
let reg = /cat/g;
str.replace(reg,function(){
console.log(arguments);
return "tiger";
})
// 第一次打印结果:
[
0: "cat",
1: 19,
2: "This is the flower cat and the civet cat."
]
// 第二次打印结果:
[
0: "cat",
1: 37,
2: "This is the flower cat and the civet cat."
]

当正则使用了分组捕获时,函数参数依次是:
0、匹配到的子字符串;
1、第一个分组项(如存在多个分组会按序紧跟返回);
(总分组项+2)、匹配到的子串的索引位置;
(总分组项+3)、源字符串本身;
例:

let str = 'abc';
let reg = /(a)(b)(c)/g;
str.replace(reg,function(){
console.log(arguments);
return arguments[3]+arguments[2]+arguments[1]; //cba
})
// arguments打印结果:
[
0: "abc",
1: "a",
2: "b",
3: "c",
4: 0,
5: "abc"
]
// 等价于
str.replace(reg,"$3$2$1");