![7e88c727b55c35fe7fa6ab65eb3ba3fb.png](https://i-blog.csdnimg.cn/blog_migrate/1b32fe743bb23c298a0760a2b1b8b778.jpeg)
前言
上一篇 【阅读整理】正则表达式 - 基础篇 介绍了构建正则的各个零部件们。在思考篇,我们继续深入,讲讲:
- JavaScript正则引擎的搜索机制
- 如何读一个正则
- 如何写一个正则
作为补充啦~ Just have fun!!!
JavaScript正则引擎的搜索机制
“脱颖而出”的传统型NFA
JavaScript的正则引擎是传统型NFA,NFA是“非确定型有限自动机”的简写,另外还有DFA(“确定型有穷自动机”)、POSIX NFA(符合POSIX标准的NFA引擎)。
(这块没深入了解,从同事那儿搬运个有意思的例子能来说明问题 ) 举这个例子: /to(nite|knight|night)/.exec('aatonightbbb
x27;)
如果是DFA:文本主导,手里握着文本,眼睛看着表达式,逐个字符匹配。当匹配到 n
的时候,发现 nite|knight|night
之中 knight
的 k
不匹配,舍弃 knight
,匹配 nite
和 night
;当匹配到 g
的时候发现 nite
的 t
不匹配,舍弃 nite
,匹配 night
……直到输出匹配结果 tonight
。
如果是传统型DFA:表达式主导,手里握着正则表达式,眼睛看着文本,逐个字符匹配。当匹配到 t
后,匹配紧随其后的文本是否是 o
,接着存在 3 种可能(nite|knight|night
),先取第一个子表达式 nite
,匹配到 t
的时候,发现文本是 g
,不匹配,放弃 nite
,进行 knight
…以此类推,直到匹配到第三个 night
子表达式完成匹配,输出结果 tonight
。
如果是POSIX NFA:表达式主导,特点是尽可能地在回溯过程中匹配最长的结果,换一个可区别的例子:
/(acc|accdee)/.exec("accdeefff”);
// NFA 匹配结果 acc
// POSIX NFA 匹配结果 accdee
图示差异:
![93cca1c050099cd698ca565e68e57143.png](https://i-blog.csdnimg.cn/blog_migrate/d924240ad2d563ba55a95b55ba6d68d0.jpeg)
(p.s. 忽略优先量词,指的是在量词后面加?
的惰性匹配,尽可能少的匹配)
大部分语言中的正则都是NFA,为啥它这么流行呢?
答:你别看我匹配慢,但是我编译快啊。
回溯
上一小节其实偷偷涉及到了回溯,如果没看明白呢,这一小节对此做一个解释!
拿正则/ab{1,3}c/
去匹配abbc
:
![9fde74e04d7dab161b0a108050655984.png](https://i-blog.csdnimg.cn/blog_migrate/6de985144cfe1196796e9cc117c90103.jpeg)
在第五步,因为默认的贪婪匹配,表达式贪婪量词这里多吃了一个b
,但是匹配文本失败,就得吐出这个b
,再接着去匹配表达式后面的内容。
除了贪婪匹配过程中会发生回溯以外,常见的回溯形式还会发生在:
- 惰性匹配:表达式惰性量词这里最初只吃一个
d
,$
结束位置符,导致表达式后面的内容无法匹配文本了,匹配失败,回溯到惰性量词这里再多吃一个d
,匹配成功
var string = "12345";
var regex = /^(d{1,3}?)(d{1,3})$/;
console.log( string.match(regex) );
// => ["12345", "12", "345"]
- 分支结构:表达式分支结构顺序在前的优先去匹配(这里的
can
),但是因为^
和$
导致匹配失败,回溯到分支结构的第二个选项candy
,发生回溯
var string = ‘candy’;
var regex = /^(?:can|candy)$/
console.log( string.match(regex) );
// => [“candy”]
贪婪匹配与惰性匹配
贪婪匹配与惰性匹配应该已经很熟悉了 ♂️,这里给个典型例子。
需求:找到字符串中双引号内的内容,比如'a "witch” and her “broom” is one’
,找出witch
和broom
。
很自然而然地想到了下面正则,然而贪婪匹配是原罪,/".+"/g
尽可能地吃阿吃,吃成了结果”witch” and her “broom”
let regexp = /“.+"/g;
let str = ‘a “witch” and her “broom” is one’;
alert( str.match(regexp) ); // “witch” and her “broom”
那怎么办呢?开启惰性匹配,少吃点,我的表达式!
let regexp = /“.+?"/g;
let str = ‘a “witch” and her “broom” is one’;
alert( str.match(regexp) ); // witch, broom
其实还有替换方案,不需要开启惰性匹配:显示排除双引号内不能在带有双引号
let regexp = /“[^"]+"/g;
let str = ‘a “witch” and her “broom” is one’;
alert( str.match(regexp) ); // witch, broom
读一个正则
哎,当遇到一个复杂的正则,怎样快速读懂它呢?
当然是二话不说扔可视化工具 ,一目了然哎!
但…如果没法用可视化工具,又或者在可视化一个正则后,又想更多了解一下细节,怎么自力更生呢?
这涉及到正则表达式的拆分,也就是优先级,优先级从高到低:
- 转义符
2. 括号和方括号:(…)
(非捕获分组、环视)、[…]
3. 量词限定符:{m,n}
、?
、+
、*
4. 位置和一般字符
5. 管道符:|
比如/ab?(c|de*)+|fg/
:
1. 由于括号的存在,所以,(c|de*)
是一个整体结构;
2. 在(c|de*)
中,注意其中的量词*
,因此e*
是一个整体结构;
3. 又因为分支结构|
优先级最低,因此c
是一个整体、而de*
是另一个整体;
4. 同理,整个正则分成了 a
、b?
、(...)+
、f
、g
。而由于分支的原因,又可以分成ab?(c|de*)+
和fg
这两部分
写一个正则
那怎么去构建一个正则呢?
构建正则前提
等等…等等!先从前往后想一想这几个问题:
1. 是否能构建正则解决问题:比如 1010010001….
虽然很有规律,但它的量词是动态的,正则无法解决这个问题;
2. 是否有必要使用正则:JavaScript丰富的String API是不是已经可以解决问题?
3. 好了,那去构建一个正则吧,但是否有必要构建一个复杂的正则 比如密码匹配问题,要求密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。
//一个正则
var regexHuge = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
//切分多个正则
var regex1 = /^[0-9A-Za-z]{6,12}$/;
var regex2 = /^[0-9]{6,12}$/;
var regex3 = /^[A-Z]{6,12}$/;
var regex4 = /^[a-z]{6,12}$/;
function checkPassword(string) {
if (!regex1.test(string)) return false;
if (regex2.test(string)) return false;
if (regex3.test(string)) return false;
if (regex4.test(string)) return false;
return true;
}
准确性
我们开始去构建一个正则表达式,最最基本的是它的准确性,准确性体现在两方面:
1. 匹配预期的字符串
2. 不匹配非预期的字符串
确保准确性有一个方法论:
1. 枚举可能出现类型
2. 提取公共部分
举个例子,要求匹配如下格式的浮点数:
1.23、+1.23、-1.23
10、+10、-10
.2、+.2、-.2
正则会由3部分组成:
- 符号部分:
[+-]
- 整数部分:
d+
- 小数部分:
.d+
所以对应的3种情况:
- 要匹配1.23、+1.23、-1.23,可以用
/^[+-]?d+.d+$/
- 要匹配10、+10、-10,可以用
/^[+-]?d+$/
- 要匹配.2、+.2、-.2,可以用
/^[+-]?.d+$/
提取公共部分后是:/^[+-]?(d+.d+|d+|.d+)$/
(虽然好像挺傻的,但保证了准确性)
如果再简洁一下: /^[+-]?(d+)?(.)?d+$/
效率
在保证准确性的前提,才会去考虑效率、做优化。大多数情况是不需要优化的,除非运行的非常慢。
参考链接我甩这里:正则表达式的构建-效率
摘两个实践性较高的:
- 当不需要使用分组引用和反向引用时,使用非捕获分组:捕获分组和分支里的数据是需要内存的;
- 使用具体型字符组来代替通配符,来消除回溯;(参照找到字符串中双引号内的内容的正则,使用具体型字符组的
/“[^"]+"/g
)
总结
开始接触正则,是因为正则不仅属于前端,是CS专业的一个基本素养,有提高这方面素养的考虑。现在走了一遍理论和很多demo,接下来关键还是在遇到相关问题时候,敢于用正则去解决问题,多实践!
正则这块,老姚的教程看了好几遍,还有其他一些零散的文章,形成了这两篇 读书笔记。朋友们时间充裕的话,还是建议去看下老姚的教程(链接在末尾),再来看这两篇拙略的读书笔记。嘿嘿,欢迎交流。
参考链接
- Greedy and lazy quantifiers:贪婪匹配、惰性匹配
- 掘金-老姚-JS正则表达式完整教程: