正则原理剖析
一、 什么是回溯?
回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。
假如我们的正则为 /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的test
b) (?<=exp):零宽正向后行断言
let reg = /(?<=prefix)(test)/ //匹配前面为prefix的test
c) (?!exp):零宽负向先行断言
let reg = /(test)(?!suffix)/ // 匹配后面不是suffix的test
d) (?let reg = /(? 零宽是何意?零宽断言是一种零宽度的匹配,它匹配到的内容不会保存到匹配的结果中去,最终匹配的只是一个位置,这个位置的结果代表是否。
2、模式修正符:一般情况下我们会在表达式的后面加上g,i,m,s对整个表达式进行修饰,js正则表达式还支持对局部表达式进行修饰的用法。
(?i)AB 对表达式(?i)后面的字符开启不区分大小写的限制,可以匹配:ab、aB、Ab、AB
(?i:a)b 只对a不区分大小写,可以匹配:Ab、ab
3、捕获ES6正则命名捕获,不需要纠结分组的下标,根据定义正则表达式对分组的名称进行访问。
示例:
let str = '2021-09-26'
let reg = /(?\d{4})-(? \d{2})-(? \d{2})/
let ret = str.match(reg).groups
输出结果:{year: '2021', month: '09', day: '26'}
文章图片
将表达式中的\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,}$");
推荐阅读
- Android悬浮窗原理解析(Window)[源码]
- MyBatis 中 Mapper 接口的使用原理
- Mybaits 源码解析 ----- 面试源码系列(Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库()))
- 机器人运动原理详解
- mybatis mapper加载原理
- 小记--------spark的Master的Application注册机制源码分析及Master的注册机制原理分析
- 前端常用正则
- 学习笔记|SpringBoot05(自动配置原理)
- Android多线程断点续传下载原理及实现
- Android的消息机制之ThreadLocal的工作原理