1. 定义
正则表达式,Regular Expression,简称 "regex"、"regexp",是一串用来描述从左到右匹配一系列符合某个句法规则的字符串,可以实现搜索、替换、提取等功能。
2. 组成元素
- 普通字符: 计算机的可打印字符,通常为数字,大小写字母,一些标点符号如 。、_ 等
- 元字符: 表示特殊语义的字符,如转义字符、^、| 等
- 修饰符(标记): 指定额外的匹配策略,通常不写在表达式内,而是表达式之外,如 /abc/g 匹配所有 abc 字串
3. 元字符
3.1 匹配字符
| 元字符 | 名称 | 描述 | 字符串 | 正则 |
|---|---|---|---|---|
| . | 点运算符 | 只匹配一次字符,除了换行符以外所有字符中的一个。注意只有 \. 才代表点本身,而 (.) 或 .{1} 或 (.){1,2} 都是点运算符 | car | /.ar/g => car |
| [ ] | 字符集/类 | 只匹配一次字符,可在内部使用 - 来指定字符集的范围,注意,字符集并不关心顺序。 | a3 d3 .3 | /[a-cA-C.]3/g => a3 d3 .3 |
| [^ ] | 否定字符集 | 只否定一次字符,可在内部使用 - 来指定否定字符集的范围,注意,否定字符集并不关心顺序。 | a3 d3 .3 | /[^a-cA-C.]3/g => a3 d3.3 |
| \w | 只匹配一次字符,所有字母数字下划线中的一个,等同于 [a-zA-Z0-9_] | word | /\w/g => word,注意它是匹配了四次,而不是一次性匹配 word | |
| \W | 只匹配一次字符,所有非字母数字下划线中的一个,等同于 [^\w] | *() | /\W/g => *(),注意它是匹配了三次,而不是一次性匹配 *() | |
| \d | 只匹配一次字符,所有数字中的一个,等同于 [0-9] | 2021 | /\d/g => 2021,注意它是匹配了四次,而不是一次性匹配 2021 | |
| \D | 只匹配一次字符,所有非数字中的一个,等同于 [^\d] | abc | /\D/g => abc,注意它是匹配了三次,而不是一次性匹配 abc | |
| \s | 只匹配一次字符,所有空格中的一个,等同于 [\t\n\f\r\p{Z}] | \t\n | /\s/g => \t\n,注意它是匹配了两次,而不是一次性匹配 \t\n | |
| \S | 只匹配一次字符,所有非空格中的一个,等同于 [^\s] | abc | /\S/g => abc,注意它是匹配了三次,而不是一次性匹配 abc | |
| \f | 匹配一个换页符 | |||
| \n | 匹配一个换行符 | |||
| \r | 匹配一个回车符 | |||
| \t | 匹配一个制表符 | |||
| \v | 匹配一个垂直制表符 | |||
| \p | 匹配 CR/LF(等同于 \r\n),用来匹配 DOS 行终止符 | |||
| \ | 转义字符 | 用于匹配一些保留的字符和中文,如 [ ] ( ) { } . * + ?^ $ |\ | 测试 | \u6d4b\u8bd5 => 测试 |
注意: 如果要捕获 3acddfs23 中的 acddfs 应当使用 \w+ 而非 \w,因为后者只会捕获一个字母字符
3.2 匹配数量(量词)
| 元字符 | 描述 | 字符串 | 正则 |
|---|---|---|---|
| {n, m} | 限定 {n,m} 之前的字符或一组字符可以重复出现至少 n 次,至多 m 次 | abababab | /[ab]{2,3}/ => ababab |
| ? | 等价于 {0, 1} | abababab | /[ab]?/ => abababab |
| + | 等价于 {1, } | abababab | /[ab]+/ => abababab |
| * | 等价于 {0, },通常用来过滤某些可有可无的字符串 | abababab | /[ab]*/ => **abababab |
3.3 匹配位置
| 元字符 | 名称 | 描述 | 字符串 | 正则 |
|---|---|---|---|---|
| ^ | 锚点 | 用来检查匹配的字符串是否在所匹配字符串的开头。 | abc | /^b/ => abc |
| $ | 锚点 | 用来检查匹配的字符串是否在所匹配字符串的结尾。 | abc | /$b/ => abc |
| \b | 单词边界 | 用来检查匹配的字符串是否符合所匹配字符串的单词边界 | cat dcatd | /\bcat\b/ => cat dcatd |
| \B | 非单词边界 | 词边界用来检查匹配的字符串是否不符合所匹配字符串的单词边界 | cat dcatd | /\Bcat\B/ => cat dcatd |
3.4 逻辑处理
| 元字符 | 描述 | 字符串 | 正则 |
|---|---|---|---|
| | | 或运算符 | The car is the | /(t|T)he/g => The car is the |
| !和[^] | 非运算符 | The car is the | /(t|!T)he/g => The car is the |
3.5 子表达式/分组
子表达式就是用小括号括起来的表达式,也称为分组。根据用途的不同可以分为捕获组和非捕获组两种,可以用来回溯/后向引用,零宽断言等功能。可以构造出十分复杂的正则表达式
4. 修饰符
| 修饰符 | 描述 | 例子 | 表达式 |
|---|---|---|---|
| i | 忽略大小写 | The the is | /The/gi => The the is |
| g | 全局搜索 | The the is | /The/gi => The the is |
| m | 多行修饰符,将锚点元字符 ^ $,由单一字符串边界变成多行行边界 | cat sat | /.at$/g =>cat sat /.at$/gm => cat sat |
| s | 单行修饰符,允许 . 点运算符识别换行符 | cat sat | /.+/s => cat sat |
5. 捕获组与非捕获组
我们对子表达式进一步分类,可以分为捕获组和非捕获组两种,捕获组匹配结果的同时捕获结果,可以自我命名也会被自动分配组号,而非捕获组只匹配结果,既不捕获结果,也不会被自动分配组号
5.1 捕获组
| 模式 | 名称 | 字符串 | 正则 |
|---|---|---|---|
| (exp) | 普通捕获组 | go go | /(go)/ => go go |
| (?<名称>exp) (?'名称'exp) | 命名捕获组 | go go | /('name'go) \k'name'/ => go go |
| \编号 | 对编号捕获组的反向引用 | go go | /(go) \1/ => go go |
| \k<名称> 或 \k'名称' $<名称> 或 $'名称' | 对命名捕获组的反向引用 | go go | /('name'go) \k'name'/ => go go |
1. 概念补充
- 反向引用:也叫做回溯引用,后向引用等等,捕获组捕获到的内容,不仅可以在正则表达式外部通过程序进行引用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。反向引用的作用通常是用来查找或限定重复,限定指定标识配对出现等等。不过要特别注意,反向引用是指,重复成功匹配后的文本,而非重复子表达式本身!!!
- 平衡组:平衡组是捕获组的进一步的使用,但是 JS 暂未实现,可参考@此文章
2. 分配规则
- 无论是普通捕获组还是命名捕获组都有一个组号,规则是从左到右第一遍分配普通捕获组的组号,从 1 递增,之后从左到右第二遍分配命名捕获组的编号,从最大的普通捕获组组号递增。组号 0 代表整个表达式
- javascript 中,\1 或 \k'name' 都是用于正则表达式内部,而 名称 是用于 replace 第二个替换参数。
5.2 非捕获组
| 模式 | 名称 | 字符串 | 正则 |
|---|---|---|---|
| (?:exp) | 普通非捕获组 | go go | /(?:go) \1/ => go go 报错,无 \1 |
| (?判断exp) | 零宽断言非捕获组 | ||
| (?#注释) |
- 普通非捕获组 (?:exp) 跟正常的 (exp) 匹配行为一致,只是无法反向引用也不分配组号,可以节省内存。
6. 零宽断言
零宽断言正如它的名字,是一种零宽度的匹配,他也是子表达式下非捕获组的一种,作用是给指定位置添加一个限定条件(断言),用来规定此位置之前或者之后的字符必须满足限定条件才能使正则中的子表达式匹配成功。其实上述元字符中匹配位置的四个元字符也是属于零宽断言的范畴。
6.1 基本形式
- 判断:条件判断词,!、=、<=、<!,不同的判断词代表了如何适配 exp 的位置
- exp:匹配位置时需要符合的表达式
6.2 学术名词
其实,零宽断言和下列名词看起来十分唬人,但是其实他只是不同匹配姿势的代名词,做的事情其实并不复杂
6.2.1 公式
| 公式 | 名称 | 描述 |
|---|---|---|
| (?=exp) | 零宽度正预测先行断言 | 断言位置后面出现的字符需要匹配 exp 表达式 |
| (?<=exp) | 零宽度正回顾后发断言 | 断言位置前面出现的字符需要匹配 exp 表达式 |
| (?!exp) | 零宽度负预测先行断言 | 断言位置后面出现的字符不能匹配 exp 表达式 |
| (?<!exp) | 零宽度负回顾后发断言 | 断言位置前面出现的字符不能匹配 exp 表达式 |
注意:零宽断言只是匹配位置而已!!!位置匹配成功的字符并不会出现最终结果中,所以他是非捕获组!!
6.2.2 例子
| 字符串 | 表达式 | 结果 |
|---|---|---|
| <div>this is vedio.</div> | /(?<=<div>)(.)+(?=</div>)/ | <div>this is vedio.</div> |
| <div>this is vedio.</div> | /<(?!/)\w+>/ | <div>this is vedio.</div> |
7. 贪婪与非贪婪模式
贪婪与非贪婪模式影响的是被量词修饰的子表达式或字符的匹配行为,准确来说,它影响的是量词的含义,互为两个极端
7.1 分类
1. 量词
- 匹配优先量词:在元字符匹配数量的量词中,属于贪婪模式的量词,{m,n}、{m,}、?、*、+
- 忽略优先量词:在元字符匹配数量的量词中,属于非贪婪模式的量词,通常是后面加上 ? 即可,{m,n}?、{m,}?、??、*?、+?
2.模式
- 贪婪模式:被匹配优先量词修饰的子表达式或字符,如 (exp)+。在整个表达式匹配成功的前提下,尽可能多的匹配。
- 非贪婪模式:别称懒惰模式,被忽略优先量词修饰的子表达式或字符,如 (exp)+?。在整个表达式匹配成功的前提下,尽可能少的匹配。非贪婪模式只被部分 @NFA 引擎所支持。
7.2 (exp){m, n} 和 (exp){m, n}? 区别
(exp){m, n} 代表在到达上限 n 之前,尽可能多的匹配 exp,(exp){m,n} 代表在满足下限 m 之后,尽可能少的匹配 exp。
我们知道元字符量词的 ? + * 都可以转成对应 {m,n} 的形式,如 (exp)* 代表 {1, },即能匹配多少符合 exp 的文本就尽量匹配多少,而 (exp)*? 代表 {1, }?,即在满足下限 1 个 exp 后,能少匹配多少就少匹配多少。
7.3 例子
- 源字符串:aa<div>test1<\div>bb<div>test2<\div>cc
- 表达式一:/<div>.*<\div>/ => aa<div>test1<\div>bb<div>test2<\div>cc
- 表达式二:/<div>.*?<\div>/ => aa<div>test1<\div>bb<div>test2<\div>cc
- 表达式三:/<div>.*?<\div>/g => aa<div>test1<\div>bb<div>test2<\div>cc
表达式一采用的是贪婪模式,在匹配到第一个 </div> 时,已经可以使整个表达式匹配成功,但是由于采用的是贪婪模式,所以仍然要向右尝试匹配,查看是否还有更长的可以成功匹配的子串,匹配到第二个 </div> 后,向右再没有可以成功匹配的子串,匹配结束,匹配结果为 aa<div>test1<\div>bb<div>test2<\div>cc
表达式二采用的是非贪婪模式,在匹配到第一个 </div> 时,已经可以使整个表达式匹配成功,由于采用的是非贪婪模式,所以结束匹配,不再向右尝试,匹配结果为 aa<div>test1<\div>bb<div>test2<\div>cc
表达式三采用的也是非贪婪模式,在匹配到第一个 </div> 时,已经可以使整个表达式匹配成功。但是由于修饰符标记了全局搜索 g,所以仍然要向右尝试匹配,查看是否还有符合此表达式的子串,匹配到第二个 </div> 后,向右再没有可以成功匹配的子串,匹配两次后结束,匹配结果为 aa<div>test1<\div>bb<div>test2<\div>cc
上述的解释只是基于应用的角度进行分析,只是为了便于理解角度出发。@实际的匹配并非如此简单。通常来说,非贪婪匹配的性能低于贪婪匹配。
8. JavaScript 兼容性
ES2018:负向零宽断言、命名捕获组、单行修饰符、Unicode转义