本文介绍的并不是正则表达式的通配符含义,或者一些正则的书写技巧
而是介绍正则匹配的流程,正则进行匹配的规则
1.首先字符串中的每一个字符都有位置
例如字符串:abcde
这个时候位置关系如下
a b c d e
↑ ↑ ↑ ↑ ↑ ↑
0位置 1位置 2位置 3位置 4位置 5位置
2.正则表达式有一个占位的概念
分为两种
①是普通匹配,普通匹配占宽度,匹配的是字符
②是断言,断言不占宽度,匹配的是某一个位置,所以断言也被叫做零宽度断言。
举个例子
例如表达式:/\w(?=\d)/
首先表达式拆解为 \w 与 (?=\d) 两个部分,
表达式 \w 匹配一个字符,占用一个宽度,(?=\d) 表示一个数字的位置
整个正则表达式连接起来就是,首先匹配一个字母或者数字 \w,匹配的这个 \w 后面必须紧跟一个数字 (?=\d)
这里的 \w 为普通匹配,占用宽度,而 (?=\d) 为断言,并不占用宽度,只是规定 \w 后面必须含有一个数字
看一下使用上面这个表达书进行匹配的结果
('abc123').match(/\w(?=\d)/);
// 匹配到的结果为 ['c', index: 2, input: 'abc123']
断言由于不占宽度所以match的结果只有字符'c'
3.正则表达式的控制权概念
联系第 1 点和第 2 点
例如我们有一个正则表达式,和一个需要进行匹配的字符串
let reg = /bc/g;
let str = 'abcdabc';
首先对字符串位置做标记,方便我们观察
a b c d a b c
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
0位置 1位置 2位置 3位置 4位置 5位置 6位置 7位置
同样的,我们对正则表达式 /bc/g 进行拆解为 b 与 c 两部分
匹配的过程如下:
- 整个正则表达式的起始匹配字符为 b,整个正则控制权在 b 手中,首先从 0 位置开始匹配字符串 abcdabc
- 0 位置之后字符为 a,匹配失败,匹配位置由 0 位置转移到 1 位置,正则控制权回到整个正则表达式的最前面,所以这个时候控制权依旧在 b 手中(其实这里还牵扯到回溯,这里暂时先不做讨论)
- 匹配位置转移到 1 之后,从 1 位置开始匹配,字符串 1 位置之后的字符为 b,此时正则的控制权也是在 b 字符上,匹配成功,匹配的位置从 1 变为 2 ,由于正则表达式中的 b 字符没有做量词修饰,即它表示只占用 1 个位置的宽度,所以 b 的控制结束,控制权移交给正则的下一个匹配字符 c
- 从 2 位置继续匹配,字符串 2 位置之后的字符为 c,此时正则的控制权也正好在字符 c 上,匹配成功,匹配位置从 2 变为 3,由于 c 没有做任何量词的修饰,表示他只占用 1 个宽度,所以 c 的控制结束,控制权继续移交给正则的下一个匹配字符
- 匹配字符 c 之后已经没有其他的匹配字符了,整个正则表达式结束,此时字符串的匹配位置依旧是上一步结束时候的 3 位置
- 由于正则表达式声明为 g 全局匹配,所以整个正则表达式将从头开始,控制权移交给整个正则表达式的起始字符 b
- 正则表达式控制权在 b,匹配位置为之前匹配成功的结束位置 3,字符串 3 位置之后的字符为 d,表达式 b 匹配字符 d 失败,控制权依旧在起始位置 b,字符串匹配位置由 3 变为 4
- 正则表达式控制权在 b,匹配位置为 4,字符串 4 位置时候的字符为 a,表达式 b 匹配字符 a 失败,控制权依旧在起始位置 b,字符串匹配位置由 4 变为 5
- 正则表达式控制权在 b,匹配位置为 5,字符串 5 位置时候的字符为 b,匹配成功,匹配的位置从 5 变为 6 位置,由于正则表达式中的 b 字符没有做量词修饰,即它表示只占用 1 个位置的宽度,所以 b 的控制结束,控制权移交给正则的下一个匹配字符c
- 正则表达式控制权在 c,匹配位置为 6,字符串 6 位置时候的字符为 c,匹配成功,匹配的位置从 6 变为 7 位置,由于正则表达式中的 c 字符没有做量词修饰,即它表示只占用 1 个位置的宽度,所以 c 的控制结束,控制权继续向后移交
- 正则表达式结束,由于它是全局匹配,控制权再次回到 b,但是由于这个时候所匹配的字符串已经结束了,所以整个匹配过程结束
4.回溯
①当正则匹配不成功的时候,就会尝试进行回溯
②回溯成功与否取决于是否有可回溯的位置
③若没有会回溯位置,则整个正则表达式匹配失败,控制权交还给表达式的起始位置
④正则规则中使用量词修饰,或者使用|的时候,所匹配的位置为可回溯位置
依旧是一个简单的例子
let reg = /ab{1,3}bbc/;
let str = 'abbbc';
a b b b c
↑ ↑ ↑ ↑ ↑ ↑
0位置 1位置 2位置 3位置 4位置 5位置
依旧对正则进行拆解,拆解成 a,b{1,3},b,b,c 五部分
依旧按照之前的规则进行匹配
- 控制权 a,起始位置 0(整个正则表达式的最开始位置),匹配 a 成功,匹配位置变为 1,a 宽度为 1,控制权移交给 b{1,3}
字符串: a
↑
正 则: a
- 控制权 b{1,3},匹配位置 1,匹配 b 成功,匹配位置变为 2(第 1 个可回溯位置),b{1,3} 宽度 1-3 之间,默认为贪婪模式,控制权依旧在 b{1,3}
字符串: a b
↑ ↑
正 则: a b{1,3}
- 控制权 b{1,3},匹配位置 2,匹配 b 成功,匹配位置变为 3(第 2 个可回溯位置),b{1,3} 宽度1-3之间,默认为贪婪模式,控制权依旧在 b{1,3}
字符串: a bb
↑ ↑
正 则: a b{1,3}
- 控制权 b{1,3},匹配位置 3,匹配 b 成功,匹配位置变为 4,b{1,3} 宽度 1-3 之间,已经达到最大宽度,结束匹配,控制权移交给 b
字符串: a bbb
↑ ↑
正 则: a b{1,3}
- 控制权 b(前一个),匹配位置 4,匹配 c 失败,尝试进行回溯,最近的一个可回溯位置为 3 位置,此时 b{1,3} 这个时候只匹配头两个 bb 字符,从 3 位置开始,b(前一个)匹配第三个 b 字符,回溯成功,b 宽度为 1,控制权移交给 b(后一个),匹配位置变为4
字符串: a bb b
↑ ↑ ↑
正 则: a b{1,3} b
- 控制权 b(后一个),匹配位置 4,匹配 c 失败,尝试进行回溯,上一个可回溯位置为 2 位置,此时 b{1,3} 这个时候只匹配 b 一个字符,控制权移交给 b (前一个),从 2 位置匹配 b (前一个)成功,匹配位置变为 3,控制权移交给 b(后一个),从 3 位置匹配 b(后一个)成功,匹配位置变为 4,控制权移交给 c
字符串: a b b b
↑ ↑ ↑ ↑
正 则: a b{1,3} b b
- 控制权 c,匹配位置 4,匹配 c 成功,匹配位置变为 5
字符串: a b b b c
↑ ↑ ↑ ↑ ↑
正 则: a b{1,3} b b c
- 字符串结束,整个匹配过程结束
回溯总结
①正则匹配规则 a, b{1,3}, b, b, c 分别匹配到了字符 a, b, b, b, c
②整个过程中由于 b{1,3} 存在可回溯位置,正则默认匹配规则为贪婪模式,b{1,3} 首先尽可能多的匹配,直到无法继续匹配的时候将控制权移交给下一个匹配字符
③当之后的匹配字符匹配失败的时候,正则表达式尝试从可回溯位置开始进行匹配,如果匹配依旧失败的话,再往前找上一个可回溯位置,直到表达式匹配成功
④如果已经没有任何可回溯位置能满足表达式,则整个表达式匹配失败,它将从上次匹配字符串的开始位置的下一个位置再次尝试匹配
5.贪婪模式和非贪婪模式
正则默认为贪婪模式,
贪婪模式为尽可能多的匹配,但是非贪婪莫模式不能只解释为尽可能少的匹配
let reg = /\w+?/;
let str = 'abcd1234efgh5678';
str.match(reg);
// 结果为['a']
这个时候确实可以理解为尽可能少的匹配
再看一个例子
reg = /\w+?\d/;
str.match(reg);
// 结果为['abcd1']
这个时候如果按照尽可能少的匹配的原则,匹配到的应该是['d1']
所以不能单纯的理解为尽可能少的匹配
6.其他一些匹配回溯的例子
let reg = /[a-z]{1,5}1/;
let str = 'abcdef1ghijkl';
表达式拆解为 [a-z]{1,5} 与 1 两部分
a b c d e f 1 g h i j k l
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
0 1 2 3 4 5 6 7 8 9 10 11 12 13
从0位置开始匹配
- [a-z]{1,5} 匹配 abcde 控制权移交给 1
字符串: abcde f
↑ ×
正 则: [a-z]{1,5} 1
- 1 匹配f失败,尝试回溯,[a-z]{1,5} 匹配 abcd ,控制权交给 1
字符串: abcd e
↑ ×
正 则: [a-z]{1,5} 1
- 1 匹配 e 失败,尝试回溯,[a-z]{1,5} 匹配 abc,控制权交给 1
字符串: abc d
↑ ×
正 则: [a-z]{1,5} 1
- 1匹配d失败,尝试回溯,[a-z]{1,5}匹配ab,控制权交给1
字符串: ab c
↑ ×
正 则: [a-z]{1,5} 1
- 1匹配c失败,尝试回溯,[a-z]{1,5}匹配a,控制权交给1
字符串: a b
↑ ×
正 则: [a-z]{1,5} 1
- 1匹配b失败,[a-z]{1,5}无可回溯位置,[a-z]{1,5}前面的表达式也没有可回溯位置,匹配失败,即从字符串0位置开始匹配失败
从1位置开始匹配
- [a-z]{1,5} 匹配 bcdef 控制权移交给 1
字符串: a bcdef
↑
正 则: [a-z]{1,5}
- 1 匹配 1 成功,控制权继续向后移交
字符串: a bcdef 1
↑ ↑
正 则: [a-z]{1,5} 1
- 个表达式结束,并且未声明为全局匹配,整个匹配过程结束
7.回溯例子
let str = 'abbbbbc';
let reg = /ab{1,3}b{1,2}bc/;
表达式 字符串
a a
ab{1,3} ab (a, b{1,3}分别匹配a, b)
ab{1,3} abb (a, b{1,3}分别匹配a, bb)
ab{1,3} abbb (a, b{1,3}分别匹配a, bbb)
ab{1,3}b{1,2} abbbb (a, b{1,3}, b{1,2}分别匹配a, bbb, b)
ab{1,3}b{1,2} abbbbb (a, b{1,3}, b{1,2}分别匹配a, bbb, bb)
ab{1,3}b{1,2}b abbbbbc (a, b{1,3}, b{1,2}, b分别匹配a, bbb, bb, c, 匹配失败尝试进行回溯)
ab{1,3}b{1,2}b abbbbb (a, b{1,3}, b{1,2}, b分别匹配a, bbb, b, b)
ab{1,3}b{1,2}bc abbbbbc (a, b{1,3}, b{1,2}, b, c分别匹配a, bbb, b, b, c)