再识正则表达式(二)

312 阅读3分钟


简介

回顾上一节,我们简述了正则表达式的一些语法和概念,那么从这节开始,我会围绕工作原理、回溯这两个点开始讲解,中间也会插入一些简单例子,帮助大家更好的理解。

工作原理

编译

编译的原理,不是这节的重点,我就简单提一下,这里的编译指的就是NFA表达式引擎,使用一个表达式的直接量或者RegExp的构造函数,可以创建一个正则表达式的实例,JS解释器在语法分析时候查看语法是否错误,然后转化成可以实际执行的代码例程(native code routine),在ES5之前版本中循环执行一个正则表达式字面量,都将引用同一个表达式,在ES5规范中明确表示,正则表达式字面量必须和构造函数创建一致,每次都创建新的实例。示例如下

let reg = null
for (let i = 0; i < 10; i++) {
    //IE8之前的浏览器中非 g 修饰符的表达式,每次lastIndex也会变化
    reg = /\d/g
    // ES5之前reg的实例将会复用,意味着lastIndex的每次都不是从0开始
    reg.test(`${i}`)
}

这里要提一下lastindex这个属性,它表示当前正则表达式所处于匹配目标中的位置。

回溯

NFA引擎的回溯,它会依次处理各个子表达式,遇到需要在两个或者多个成功可能中进行选择时,会先选择其一,同时将当前正则位置记录在备用分支中。

一般来讲,需要做出选择的情况包括量词和多选结构,其中量词分为优先匹配量词、忽略优先匹配量词、占有优先匹配量词(JavaScript暂不支持),只要其中一个分支成功,则整个表达式成功,如果一个分支失败,会进入到刚才记录的备用分支尝试回溯,直到所有分支回溯失败,整个表达式回溯失败,除了正则表达式的备用分支需要记录,对应匹配的字符串位置也需要记录,方便回溯。

回溯进行时,应该选择哪个保存状态,其中有个重要原则

1、对于优先匹配量词,引擎会进行尝试,对于忽略优先量词,引擎会跳过尝试。
2、距离当前最近存储分支,使用原则是LIFO,一个栈结构

这里我再补充一下忽略优先量词和优先量词的定义,方便大家理解

优先匹配量词:+、?、*,{0,}
忽略优先匹配量词:(*?、??、+?)

优先匹配量词

理解了基础的回溯原理,我们来看一个简单的例子

let reg = /\w+\d/
let str = 'abcdefg123'
reg.exec(str)

 \w+为优先匹配量词,所以,引擎会选择尝试所有可能,记录每一次匹配的备用状态,直到整个字符串结尾

匹配到字符串结尾后,发现还有子表达式尚未校验,所以开始回溯到自己记录的最近一次回溯点,此时交出字符串3,引擎把控制权移动到下一位子表达式\d,发现子表达式成功,整个表达式匹配成功


这里需要注意的是,回溯不仅需要重新计算正则表达式和文本的对应位置,也需要不断的维护括号内(分组)子表达式内匹配文本,再看一个例子

let reg = /.*([0-9][0-9])/
let str = 'CA95472**USA'
reg.exec(str) //72

.*用来表示一组任意字符,因为点号可以匹配任意字符,而星号表示任意数量,同样也是优先匹配量词,注意这个表达式中,我们加入了分组的括号,对应的正则的$1属性,优先匹配量词在字符串结尾处,开始回溯,移交控制权,每一次回溯,除了移动回溯状态,还会维护分组内的值,影响匹配效率

忽略优先匹配量词

忽略优先量词的概念我们在上面已经提到了,我们直接来看一个实际的例子,帮助我们理解

let reg = /<b>(.*)<\/b>/
let str = '<b>我是加粗的字体</b><b>我也是加粗的</b>'
reg.exec(str) // 我是加粗的字体</b><b>我也是加粗的字体

上述表达式我们的本意是截取一段B标签内部的文本,但是,截取了其余的同类标签,这原理和我们上面描述的回溯是一样的,那我们如何截取第一段文本呢,这时候需要用到忽略优先量词

let reg = /<b>(.*?)<\/b>/
let str = '<b>我是加粗的字体</b><b>我也是加粗的字体</b>'
reg.exec(str) // 我是加粗字体

这里我们可以看到,字符串已经成功截取到了我们想要的结果,那,忽略优先量词是如何运行的呢,我们看下图


上图我们可以发现,第二步的时候,居然绕过了.*?这个表达式,直接走到了后面的</b>表达式,原因在于忽略优先匹配,默认规则为不匹配,即使后期通过回溯回来,它也是步步为营,每走一步都会存储备用状态,并且在下次匹配时,优先忽略自己,然后继续匹配失败,继续回溯,要想让整个表达式失败,引擎必须尝试所有可能。

那么有没有一种可能让引擎能够快速失败呢,答案肯定是有的,那就是固化分组和占有优先量词,固化分组的支持度不高,所以在下一节中,我们会带过一下。

小结

本节重点讲述了回溯的一些常见情况,还有一些分支判断的例子未涉及到,其实和量词是一样的,有兴趣的朋友可以下去摸索一下,下一节,重点围绕顺序环视、固化分组,以及使用顺序环视来模拟固化分组