原文文章:JS正则表达式完整教程
本文主要是学习了老姚的文章后,自己做记录。
字符串的匹配
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
两种匹配模式
横向模糊匹配
横向模糊指的是,一个正则可匹配的字符串长度不是固定的,可以是多种情况的。其实现方式是使用量词。
譬如{m, n}
, 表示连续出现最小 m
次,最多 n
次。
纵向模糊匹配
纵向模糊匹配指的是,一个正则匹配的字符串,具体到某一个字符,它可以不是某个确定的字符,可以有多种可能。其实现方式是使用字符组。
例如 [abc]
, 表示该字符可以是 a, b, c
中的任何一个。
字符组
需要注意的是,虽然名称叫做字符组(字符类),但只是其中一个字符。例如 [abc]
, 表示该字符可以是 a, b, c
中的任何一个。
范围表示法
比如 [123456abcdeABCDE]
, 可以写成 [1-6a-eA-E]
。用连字符 -
来省略和简写。
排除字符组
例如 [^abc]
,表示是一个除a,b,c
之外的任意一个字符。字符组的第一位放 ^
表示求反的概念。
常见的简写形式
量词
量词也称重复。
简写形式
量词 | 具体含义 |
---|---|
{m, n} | 表示当前字符出现至少 m 次, 最多n 次 |
{m, } | 表示当前字符出现至少 m 次 |
{m} | 表示当前字符出现 m 次 |
? | 表示0次或1次 ==> {0,1} |
+ | 至少出现一次 ==> {1,} |
* | 表示0次或多次 ==> {0,} |
常见用法
通过在量词后面添加一个 ?
就能实现惰性匹配,因此所有的惰性匹配情形如下:
惰性量词 | 贪婪量词 |
---|---|
{m, n}? | {m,n} |
{m, }? | {m,} |
?? | ? |
+? | + |
*? | * |
对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?
以上量词对应的可视化形式是
多选分支
一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一
具体形式如下:
(p1|p2|93)
,其中 p1,p2,p3
是子模式,用 |
(管道符) 分割,表示任何之一。
例如: foo|bar
可以匹配 foo
或者 bar
可视化:
字符串的匹配
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
什么是位置?
位置(锚)是相邻字符之间的位置。如下方箭头所示的位置。
如何匹配位置呢?
在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}/;
其可视化为:
修改为带括号的
var reg = /(\d{4})-(\d{2})-(\d{2})/
对比这两张图,会发现,相对于前面的正则后面的正则多了分组编号。如 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"
同样可以匹配成功;
如果我们需要前后的分隔符匹配一致的时候,此时就需要反向引用。
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
\10 代表什么呢?
\10
代表第十个分组还是 \1
与 0
呢?
答案是 第十个分组
如果要匹配
\1
与0
, 使用(?:\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/
当匹配的字符串为abbbc
时,就没有回溯。匹配过程如下:
如果目标字符串为abbc
时,就有回溯,匹配过程如下:
图中第五步红色,表示匹配不成功。此时b{1,3}
已经匹配了 2 个 b
, 准备尝试第三个的时候,发现接下来的是 c
。则认为 b{1,3}
已经匹配完成,然后回到之前的状态(第6步与第4步一致),最后在用自表达式 c
,去匹配。
图中第6步就代表回溯。
例如正则为/ab{1,3}bbc/
, 目标字符串是abbbc
例如正则为 /".*"/
目标字符串为 "abc"de
为了减少不必要的回溯,可以改写为 /"[^"]*"/
惰性量词
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})$/
分支结构
分支结构的匹配也是惰性的,比如 can|candy
,去匹配candy
,得到的结果是 can
,因为只要前面的满足了,就不会接续尝试。
但是,如果前面的子模式形成了局部匹配,如果接下来的表达式整体不匹配的时候,仍会尝试剩余的分支。这种尝试也可以看作一种回溯。
目标字符串是 candy
,其匹配过程为: