正则表达式学习

135 阅读4分钟

原文文章:JS正则表达式完整教程

本文主要是学习了老姚的文章后,自己做记录。

字符串的匹配

正则表达式是匹配模式,要么匹配字符,要么匹配位置。

两种匹配模式

横向模糊匹配

横向模糊指的是,一个正则可匹配的字符串长度不是固定的,可以是多种情况的。其实现方式是使用量词。 譬如{m, n}, 表示连续出现最小 m 次,最多 n 次。

1.横向模糊匹配.png

纵向模糊匹配

纵向模糊匹配指的是,一个正则匹配的字符串,具体到某一个字符,它可以不是某个确定的字符,可以有多种可能。其实现方式是使用字符组。

例如 [abc], 表示该字符可以是 a, b, c 中的任何一个。

1.2.纵向模糊匹配(1).png

字符组

需要注意的是,虽然名称叫做字符组(字符类),但只是其中一个字符。例如 [abc], 表示该字符可以是 a, b, c 中的任何一个。

范围表示法

比如 [123456abcdeABCDE], 可以写成 [1-6a-eA-E]。用连字符 - 来省略和简写。

排除字符组

例如 [^abc],表示是一个除a,b,c之外的任意一个字符。字符组的第一位放 ^ 表示求反的概念。

常见的简写形式

1.字符组简写形式.png

量词

量词也称重复。

简写形式

量词具体含义
{m, n}表示当前字符出现至少 m 次, 最多n 次
{m, }表示当前字符出现至少 m 次
{m}表示当前字符出现 m 次
?表示0次或1次 ==> {0,1}
+至少出现一次 ==> {1,}
*表示0次或多次 ==> {0,}

常见用法

通过在量词后面添加一个 ? 就能实现惰性匹配,因此所有的惰性匹配情形如下:

惰性量词贪婪量词
{m, n}?{m,n}
{m, }?{m,}
???
+?+
*?*

对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?

以上量词对应的可视化形式是

1.量词.png

多选分支

一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一

具体形式如下: (p1|p2|93),其中 p1,p2,p3是子模式,用 | (管道符) 分割,表示任何之一。

例如: foo|bar 可以匹配 foo 或者 bar

可视化:

1.多选分支.png

字符串的匹配

正则表达式是匹配模式,要么匹配字符,要么匹配位置。

什么是位置?

位置(锚)是相邻字符之间的位置。如下方箭头所示的位置。

2.锚.png

如何匹配位置呢?

在ES5中有 6 个位置。

符号具体含义
^开头
$结尾
\b单词边界
\B非单词边界
(?=p)正向先行断言 匹配p前面的一个位置
(?<=p)正向后行断言 匹配p后面的一个位置
(?!p)负向先行断言 匹配不是p前面的一个位置
(?<!p)负向后行断言 匹配不是p后面的一个位置

开头(^)与结尾($

比如我们把字符串的开头与结尾用# 号替换;

let res = "hello".replace(/^|$/g, '#'))
// res => '#hello#'

多行匹配模式(即有m修饰符),二者是行的概念。

let res = "hello\nworld\n"
res.replace(/^|$/mg, "#")
// res => "#hello#\n#world#\n#

单词边界 \b|\B

\b 是单词边界,具体就是 \w\W 之间的位置,也包括 \w^ 之间的位置,和 \w$ 之间的位置

我们知道,\w 是字符组 [0-9a-zA-Z_] 的简写形式,即 \w 是字母数字或者下划线的中任何一个字 符。而 \W 是排除字符组 [^0-9a-zA-Z_] 的简写形式,即 \W\w 以外的任何一个字符。

比如 [JS] test_01.txt 中的 \b如下:

let res = '[JS] test_01.txt';
res.replace(/\b/g, "#")
// "[#JS#] #test_01#.#txt#"

res.replace(/\B/g, "#")
// "#[J#S]# t#e#s#t#_#0#1.t#x#t"

(?=p)(?!p)(?<=p)(?<!p)

let res = "hello";

// 匹配h前面的一个位置
res.replace(/(?=h)/g, "#")  
// "#hello"

// 匹配不是h前面的一个位置 
res.replace(/(?!h)/g, "#")
// "h#e#l#l#o#"

// 匹配h后面的一个位置
res.replace(/(?<=h)/g, "#")
// "h#ello"

//匹配不是h后面的一个位置
res.replace(/(?<!h)/g, "#")
// "#he#l#l#o#"

正则括号的用法

分组和分支结构

分组

我们知道/a+/匹配连续出现的 a,要匹配连续的ab就要使用括号/(ab)+/. 其中括号就提供分组作用,使量词作用于这个整体。

分支结构

var reg = /I love (she|you)/;
reg.test("I love she");  // true
reg.test("I love you");  // true

var reg = /I love she|you/;
reg.test("I love she");  // true
reg.test("I love you");  // false
reg.test("you");  // true

分组引用

以日期的正则为例,假设格式为yyyy-mm-dd;

var reg = /\d{4}-\d{2}-\d{2}/;

其可视化为:

3.普通日期.png

修改为带括号的

var reg = /(\d{4})-(\d{2})-(\d{2})/

3.带括号的日期.png

对比这两张图,会发现,相对于前面的正则后面的正则多了分组编号。如 Group #1

在匹配的过程中,正则引擎会给每一个分组开辟一个空间,用来存储每一个分组匹配到的数据。这样我们就可以使用这个分组。

提取数据

比如提取年、月、日

var reg =  /(\d{4})-(\d{2})-(\d{2})/;
var str = "2021-12-02";
console.log(str.match(reg))
// ["2021-12-02", "2021", "12", "02", index: 0, input: "2021-12-02", groups: undefined]

match 返回一个数组,第一个元素代表整体的匹配结果,然后是每个分组匹配的内容,然后是匹配下标,最后是输入的文本。另外,正则表达式中带有全局匹配符,返回的数组结构是不一样的。

同样可以使用正则的 exec 方法。

var reg =  /(\d{4})-(\d{2})-(\d{2})/;
var str = "2021-12-02";
console.log(reg.exec(str))
// ["2021-12-02", "2021", "12", "02", index: 0, input: "2021-12-02", groups: undefined]

同样可以使用构造函数 RegExp 的全局属性 $1 - $9来获取

var reg =  /(\d{4})-(\d{2})-(\d{2})/;
var str = "2021-12-02";

reg.test(str) // 正则操作皆可
// reg.exec(str)
// str.match(reg)
// str.replace(reg, "")

RegExp.$1
RegExp.$2
RegExp.$3

替换

比如要把yyyy-mm-dd替换为 mm/dd/yyyy

var reg = /(\d{4})-(\d{2})-(\d{2})/
var str = "2021-12-02";
var newStr = str.replace(reg, "$2/$3/$1")
// "12/02/2021"

var res = str.replace(reg, function(){
    return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1;
})
// "12/02/2021"

var res = str.replace(reg, function(match, $1, $2, $3){
    return $2 + '/' + $3 + '/' + $1
})


var res = str.replace(reg, function(match, year, month, day){
    return month + '/' + day + '/' + year
})

反向引用

除了使用相应 API 来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

比如要匹配如下结构

"2021-12-02"
"2021.12.02"
"2021/12/02"

最先想到的正则为/\d{4}(-|\.|\/)\d{2}(-|\.|\/)\d{2}/

这样会导致 "2021-12.02"同样可以匹配成功;

如果我们需要前后的分隔符匹配一致的时候,此时就需要反向引用。

3.反向引用2.png

var reg = /\d{4}(-|\.|\/)\d{2}\1\d{2}/
var str = "2021-12-02"  // true
var str1 = "2021.12.02" // true
var str2 ="2021/12/02"  // true
var str3 ="2021/12.02"  // false

嵌套括号怎么办?

以左括号(开括号)为准;

var reg = /^((\d)(\d(\d)))\1\2\3\4$/;
var str = '1231231233';
console.log(reg.test(str))
RegExp.$1  // 123
RegExp.$2  // 1
RegExp.$3  // 23
RegExp.$4  // 3

3.反向引用.png

\10 代表什么呢?

\10 代表第十个分组还是 \10 呢? 答案是 第十个分组

如果要匹配 \10, 使用(?:\1)0 或者 \1(?:0)

引用分组不存在怎么办?

如果引用分组不存在,就匹配字符本身 例如 \2 就匹配 2

分组后有量词怎么办?

分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。

var reg = /(\d)+/;
var str = "12345";
console.log(str.match(reg))
// ["12345", "5", index: 0, input: "12345", groups: undefined]

非捕获括号

之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分支。

如果只想要括号的原始分组功能,不要引用它,可以使用非捕获括号(?:p)

var reg = /(?:ab)+/g
var str = "ababa abbb ababab";
console.log(str.match(reg))

回溯原理

回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、 不断“回溯”寻找解的方法,就称作“回溯法”。本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中 ,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。

常见的回溯形式

贪婪量词

假如正则是 /ab{1,3}c/

4.回溯原理.png

当匹配的字符串为abbbc时,就没有回溯。匹配过程如下:

4.量词的回溯.png

如果目标字符串为abbc时,就有回溯,匹配过程如下:

4.贪婪量词的回溯.png

图中第五步红色,表示匹配不成功。此时b{1,3}已经匹配了 2 个 b, 准备尝试第三个的时候,发现接下来的是 c。则认为 b{1,3}已经匹配完成,然后回到之前的状态(第6步与第4步一致),最后在用自表达式 c,去匹配。 图中第6步就代表回溯。

例如正则为/ab{1,3}bbc/, 目标字符串是abbbc

4.贪婪量词的回溯2.png

例如正则为 /".*"/ 目标字符串为 "abc"de

4.贪婪量词的回溯3.png

为了减少不必要的回溯,可以改写为 /"[^"]*"/

4.贪婪量词的回溯5.png

惰性量词

var reg = /(\d{1,3}?)(\d{1,3})/
var str = "12345"
console.log(str.match(reg))
// ["1234", "1", "234", index: 0, input: "12345", groups: undefined]

修改正则为 /^(\d{1,3}?)(\d{1,3})$/

4.惰性量词的回溯.png

4.惰性量词的回溯2.png

分支结构

分支结构的匹配也是惰性的,比如 can|candy,去匹配candy,得到的结果是 can,因为只要前面的满足了,就不会接续尝试。

但是,如果前面的子模式形成了局部匹配,如果接下来的表达式整体不匹配的时候,仍会尝试剩余的分支。这种尝试也可以看作一种回溯。

4.分支结构的回溯.png

目标字符串是 candy,其匹配过程为:

4.分支结构的回溯2.png