正则是什么
正则是一种状态机,一种为处理字符串专门设计的机制。
正则引擎分类
DFA(确定型有穷自动机)
- 穷举机制
- 以文本为主导的匹配
NFA(不确定型有穷自动机)
- 回溯机制
- 以正则表达式主导的匹配
工作机制
其本质是:符号标记。
规则可以把一个特定的字符或者是空字符串认为是一种类型的记号的全部。
核心规则
1.逻辑处理(与、或、非)
所有计算机语言绕不开的逻辑
与(串联)
两个规则头尾连接代表规则的串联
// 匹配 abc 字符串
abc
// 匹配 123 字符串
123
或(并联)
两个规则可选,通过 | 连接,或通过 [] 包含代表并联
// 匹配 a 或 b 或 c 字符
[abc]
// 匹配 abc 或 123 字符串
abc|123
// |可以通过 () 提权,进行子串匹配。匹配 abc 或 123 字符串
(abc|123)
(?:abc|123)
(?=abc|123)
非(取反)
规则否定,取相反含义,通过 [^] (?!) (?<!) 包含代表取反
// 匹配字符 不是 a,不是b,也不是c
[^abc]
// 匹配字符串 不是 abc,也不是123
(?!abc|123)
(?<!abc|123)
2.位置处理(锚点、从左往右、 从右往左)
至关重要,理解位置才能理解规则是从哪里运行的,下一条执行的起始点,才能将规则执行步骤串起来理解。
位置(锚)是相邻字符之间的位置。
位置规则本身不占位,规则执行后锚点不变,下条规则依然从该锚点执行。
而其他规则是占位的,一条规则执行后锚点会变化(前移或后移)。
位置规则按个人理解就是判断处理。
参考:https://www.cnblogs.com/meowv/p/12874812.html
^(起始符)
标记起始位置,规则执行只判断当前锚点是否处于起始位。
// 以abc开头的字符串
^abc
// 同样是以abc开头的字符串,注意这里多个 ^。因为只判断是不是开始位置,所以再多的^执行,锚点一直在起始位置未变,判断一直成立的
^^abc
^^^^^^^^abc
¥(结束符)
标记结束位置,规则执行只判断当前锚点是否处于结束位。
// 以abc截止的字符串
abc$
// 同样是以abc截止的字符串,注意这里多个 $$。因为只判断是不是结束位置,所以再多的$执行,锚点一直在结束位置未变,判断一直成立的
abc$$
abc$$$$$$
\b(单词边界符)
单词边界位置,规则执行只判断当前锚点是否处于一个单词的边界。
// 边界位置替换为 #
var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// 实际效果看出单词边界插入了#
// => "[#JS#] #Lesson_01#.#mp4#"
// 同理将这里的 \b 改为多个,也是成立的
result = "[JS] Lesson_01.mp4".replace(/\b\b/g, '#');
result = "[JS] Lesson_01.mp4".replace(/\b\b\b\b\b\b\b/g, '#');
// 但是如果加上量词就会报错。猜猜为什么
result = "[JS] Lesson_01.mp4".replace(/\b+/g, '#');
\B(非单词边界符)
非单词边界位置,规则执行只判断当前锚点是否处于一个单词的边界
// 非单词边界位置替换为 #
var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
// 和上面的单词边界对比着看,很容易看出刚好是上面不曾出现的位置
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
// 同理将这里的 \B 改为多个,也是成立的
result = "[JS] Lesson_01.mp4".replace(/\B\B/g, '#');
result = "[JS] Lesson_01.mp4".replace(/\B\B\B\B\B\B\B/g, '#');
(?=)(正向预查,匹配满足)
锚点往右查找,判断右侧字符串是否满足括号里的匹配规则。
// 判断当前锚点右侧是不是l,是就在该位置替换#
var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"
(?!)(正向预查,匹配不满足)
锚点往右查找,判断右侧字符串是否不满足括号里的匹配规则。
// 判断当前锚点右侧是不是l,不是就在该位置替换#
var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"
(?<=)(反向预查,匹配满足)
锚点往左查找,判断左侧字符串是否满足括号里的匹配规则。
// 判断当前锚点左侧是不是l,是就在该位置替换#
var result = "hello".replace(/(?<=l)/g, '#');
console.log(result);
// => "hel#l#o"
(?<!)(反向预查,匹配不满足)
锚点往左查找,判断左侧字符串是否不满足括号里的匹配规则。
// 判断当前锚点左侧是不是l,不是就在该位置替换#
var result = "hello".replace(/(?<!l)/g, '#');
console.log(result);
// => "#h#e#llo#"
PS:
(?:) 是抽取子串,不是判断位置,因此是占位的var result = "hello".replace(/(?:l)/g, '#'); console.log(result); // => "he##o"
3.判断处理
判断其实就是位置处理,只是分为就位置本身判断,还是就位置两边字符判断。
通过上面位置处理可以看出:
- ^ $ \b \B 是就锚点位置本身的判断
- (?=) (?!) (?<=) (?<!) 是就锚点位置两边字符的判断
零宽断言
这个术语是这样描述的:
把零宽理解为关键字前面或者后面的正则表达式,匹配出来的字符不会被显示出来,也不被用来当做常规的子查询。它只起到判断作用。
简而言之就是位置和判断处理。规则本身不占位,只做判断用,不对锚点产生影响。
所以:^ $ \b \B (?=) (?!) (?<=) (?<!) 都属于零宽断言
正向预查/负(反)向预查
直白的说就是以锚点为中心,向右或向左判断预查规则。
正向预查 (?=) (?!) 以当前锚点往右查找,判断右侧字符串是否满足预查规则
负向预查 (?<=) (?<!) 以当前锚点往左查找,判断左侧字符串是否满足预查规则。<就代表着往左看。
4.量词
知名达意,匹配规则的数量,标识量词前的规则匹配的次数。如下:
? 0或1个
* 0个及以上
+ 1个及以上
{m} m个
{m,} 最少m个
{m,n} m到n个
{,n} 最多n个
贪婪匹配/贪婪量词
贪婪的,尝试可能的顺序是从多往少的方向去尝试。尽可能多的去匹配符合规则的字符串,且会回溯。
x?
x*
x+
x{2}
x{2,}
x{2,5}
x{,8}
非贪婪匹配/惰性量词
就是在贪婪量词后面加个问号。表示尽可能少的匹配符合规则的字符串。理论上不会回溯,实际是以后面的规则为准,若后面规则触发了回溯,非贪婪匹配一样会回溯。
x??
x*?
x+?
x{2}?
x{2,}?
x{2,5}?
x{,8}?
独占
就是在贪婪量词后面加个加号。同贪婪一样最长匹配,但是一旦匹配不成功就直接结束了,不会回溯。
x?+
x*+
x++
x{2}+
x{2,}+
x{2,5}+
x{,8}+
5.回溯机制
回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。
本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。
正因为有多种可能,所以要一个一个试。直到,要么到某一步时,整体匹配成功了;要么最后都试完后,发现整体匹配不成功。
- 贪婪量词“试”的策略是:买衣服砍价。价钱太高了,便宜点,不行,再便宜点。
- 惰性量词“试”的策略是:卖东西加价。给少了,再多给点行不,还有点少啊,再给点。
- 分支结构“试”的策略是:货比三家。这家不行,换一家吧,还不行,再换。
参考:https://zhuanlan.zhihu.com/p/27417442
https://jex.im/regulex
网友评论