正则原理剖析
一、 什么是回溯?
回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。
假如我们的正则为 /ab{1,3}c/,其可视化形式是:
没有回溯
目标字符串是"abbbc"时,就没有所谓的“回溯”,匹配过程如下:
有回溯
如果目标字符串是"abbc",中间就有回溯。匹配过程如下:
图中第五步表示匹配失败,此时b{1,3}已经匹配到了两个b,当往第六步走的时候发现后面是个‘c’,那么此时断定b{1,3}已经匹配完成,回到之前的状态第四步,这一过程称之为回溯,这只是回溯的一种体现。
二、 JS正则原理
1、 DFA-确定有穷状态自动机-Deterministic finite automaton
特点:文本主导
DFA读入一个文本时,会记录当前表达式中所有满足匹配要求的位置,这些位置集合对应于DFA的一个状态
2、 NFA-非确定有穷状态自动机-No-deterministic finite auotmaton
特点:表达式主导
从表达式的左边开始,每次读取一个正则表达式的一个字符,检查当前文本是否匹配当前表达式,如果是则继续正则的下一个字符。
3、示例分析
正则:reg=to(nite|knight|night) 字符串:tonight
DFA匹配过程:
1、 当引擎读入文本t,记录匹配位置to(nite|knight|night)
2、 读入文本o,记录位置to(nite|knight|night)
3、 读入文本n,记录位置to(nite|knight|night) // 体现与NFA的不同之处
...
NFA匹配过程:
1、 获取表达式的to(nite|knight|night),扫描匹配字符串中的t
2、 获取表达式的to(nite|knight|night),扫描匹配字符串中的to
3、 括号中的表达式分三种情况,引擎会尝试三种情况
。。。
从示例中可以得到结论:
DFA每一次匹配会记录所有满足要求的表达式位置,组成集合,当往下面匹配时有不满足要求的集合将会被剔除,每一步都是有效的匹配,所以不存在回溯。
NFA遇到具有选择不确定性的表达式会进行试探匹配,顺着分支往下匹配,如果后续有不匹配的,那么会重新回到确定性的位置,继续下一个分支进行匹配,要回到原来的位置的前提条件就是需要记住历史状态,所以NFA具有回溯。
三、 高级功能
NFA除了支持回溯之外,还支持分组,捕获,零宽断言等高级功能,而这些功能的基础是NFA具有子表达式独立匹配的特点。
1、分组
将一个或多个字符使用括号包裹起来组成一个整体,分组分为捕获组和非捕获组。
捕获组:
示例(ABC)(ABCD(ABCDE)),存在四个分组:
组 | 内容 |
---|---|
分组1 | (ABC)(ABCD(ABCDE)) |
分组2 | (ABC) |
分组3 | (ABCD(ABCDE)) |
分组4 | (ABCDE) |
捕获组的应用:
- 反向引用
计数
注意点:
反向引用,引用的仅仅是文本内容,而不是正则表达式。
反向引用中为什么我们一般是使用的分组是从\1开始的?非捕获组:
以 (? 开始的组就是非捕获组,不将匹配到的字符存储到内存中,从而节省内存。
(?:a|A)123(?:b)可以匹配a123b或者A123b非捕获组有很多种形式,其中包含零宽断言、模式修正符等。环视又称环视或预搜索
零宽断言,常见的零宽断言有四种:
a) (?=exp):零宽正向先行断言
let reg = /(test)(?=suffix)/ // 匹配后面为suffix的testb) (?<=exp):零宽正向后行断言
let reg = /(?<=prefix)(test)/ //匹配前面为prefix的testc) (?!exp):零宽负向先行断言
let reg = /(test)(?!suffix)/ // 匹配后面不是suffix的testd) (?<!exp):零宽负向后行断言
let reg = /(?<!prefix)(test)/ // 匹配前面不是prefix的test零宽是何意?
零宽断言是一种零宽度的匹配,它匹配到的内容不会保存到匹配的结果中去,最终匹配的只是一个位置,这个位置的结果代表是否。
2、模式修正符:
一般情况下我们会在表达式的后面加上g,i,m,s对整个表达式进行修饰,js正则表达式还支持对局部表达式进行修饰的用法。
(?i)AB 对表达式(?i)后面的字符开启不区分大小写的限制,可以匹配:ab、aB、Ab、AB
(?i:a)b 只对a不区分大小写,可以匹配:Ab、ab3、捕获
ES6正则命名捕获,不需要纠结分组的下标,根据定义正则表达式对分组的名称进行访问。
示例:
let str = '2021-09-26'
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let ret = str.match(reg).groups
输出结果:{year: '2021', month: '09', day: '26'}
四、 实战分析
Input.Password密码输入组件,密码的规则是满足四种字符中的至少三种字符类型,如下图:
将表达式中的\W替换为“()!@#$%^&*|?><”即可
new RegExp("^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W]+$)(?![a-z0-9]+$)(?![a-z\W]+$)(?![0-9\W]+$)[a-zA-Z0-9\W]{8,}$");
「多图预警」完美实现一个@功能
wuwhs赞 40阅读 4.8k评论 5
ESlint + Stylelint + VSCode自动格式化代码(2023)
谭光志赞 34阅读 20.7k评论 9
涨姿势了,有意思的气泡 Loading 效果
chokcoco赞 24阅读 2.2k评论 3
你可能不需要JS!CSS实现一个计时器
XboxYan赞 25阅读 1.7k评论 1
在前端使用 JS 进行分类汇总
边城赞 17阅读 2k
【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制
csRyan赞 26阅读 3.3k评论 1
「彻底弄懂」this全面解析
wuwhs赞 17阅读 2.4k
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。