Skip to content

正则引擎的匹配规则

NFA引擎最重要的性质就是支持回溯,那么哪些情况下发生回溯呢?会发生回溯的情况有两种:

  • 匹配优先/忽略优先/占有优先量词;
  • 多选分支结构。

回溯机制

回溯机制的基本原理不难理解,记住两个基本规则:

规则一:面对众多分支选择时,哪个分支应当首先选择?

若需要在“进行尝试”和“跳过尝试”之间选择,对于匹配优先量词,引擎会优先选择“进行尝试”,而对于忽略优先量词,会选择“跳过尝试”。

规则二:回溯时,应选择哪个保存的状态?

距离当前最近储存的选项(即备用状态)就是本地失败强制回溯时返回的。

对于NFA引擎来说,“备用状态”是引擎进行回溯的基础,“备用状态”会保存两个位置:正则表达式中的位置和未尝试的分支在字符串中的位置。在需要回溯时,“备用状态”告诉NFA引擎,匹配可以从此处开始。

来看一些简单的例子:

匹配未进行回溯

使用正则表达式/Lu?v/匹配字符串Luv:当字符L匹配成功后,匹配的当前状态如下:

字符串状态表达式状态
'Luv'Lu?v

现在该u?匹配了,此时正则引擎需要决定:是尝试匹配呢?还是跳过匹配?因为?匹配优先量词,所以引擎会尝试匹配。但为了确保这个尝试最最终失败后能够恢复,NFA引擎会把以下状态添加到备用状态序列中:

字符串状态表达式状态
'Luv'Lu?v

备用状态告诉引擎:下次匹配从正则表达式的u?之后,字符串的字符v之前开始匹配(即跳过u匹配)。之后继续检查字符 u,能够匹配,状态更新为:

字符串状态表达式状态
'Luv'Lu?v

继续检查字符v,也能满足匹配,此时整个匹配完成,备用状态不在被需要,弃用备用状态,整个匹配结束。

匹配进行了回溯

用上例相同的正则表达式匹配字符串Lv,在尝试u之前,一切与上例过程相同,但此时的u无法匹配,引擎开始回溯至最近保存的状态(即该例中唯一的备用状态),即:

字符串状态表达式状态
'Lv'Lu?v

此时v满足匹配,整个匹配结束。

进行了回溯但匹配失败

整个匹配宣告失败是发生在字符串和表达式均完成了所有回溯测试(备用状态)之后,而不是其中某一个完成其所有回溯测试之后。

举个简单的例子,我们用同样的正则表达式匹配字符串Lue,在尝试匹配字符u之前有备用状态

字符串状态表达式状态
'Lue'Lu?v

u匹配成功后,由于v无法匹配e,此时引擎回溯到最近保存状态,交换字符uv匹配,依然不能满足匹配,该次回溯测试失败,但是此时不存在其它备用状态,所以字符串中当前位置的整个匹配宣告失败。

那么整个匹配就要宣告失败了吗?并不会!,匹配将重新开始于状态:

字符串状态表达式状态
'Lue'Lu?v

但这些回溯测试均以失败告终,最终整个表达式宣告匹配失败,结束匹配

忽略优先的回溯匹配

使用正则表达式/Lu??v/匹配字符串Luv。字符L匹配之后的状态:

字符串状态表达式状态
'Luv'Lu??v

现在该u?匹配了,此时正则引擎需要决定:是尝试匹配呢?还是跳过匹配?因为??忽略优先量词,所以引擎会跳过匹配。但为了确保这个尝试最最终失败后能够恢复,NFA引擎会把以下状态添加到备用状态序列中:

字符串状态表达式状态
'Luv'Luv

保存备用状态后,沿着忽略匹配优先的路继续匹配:

字符串状态表达式状态
'Luv'Lu??v

但是此时v无法匹配字符u,引擎回溯至最近保存状态:

字符串状态表达式状态
'Luv'Luv

此时u能匹配字符u,接下来的v也能匹配字符v,到此,整个匹配宣告成功,匹配结束。

注意

和第一个例子对比:

javascript
const regexp = /Lu?v/; // 匹配优先
// const regexp = /Lu??v/; // 忽略优先
const result = regexp.exec("Luv");
console.log(result[0]); // 结果均为:Luv

发现虽然匹配优先Lu?v和忽略优先Lu??v得到的结果可能是(在本例中相同)相同的,但是回溯的过程是完全不同的!