懒惰和贪婪-正则回溯

需要一定的正则基础,并且是基于JS写的文章。

正则表达式是从左往右匹配的。在使用正则表达式的时候我们知道/.*/可以匹配一个字字符串中所有的字符,/.*?/却一个字符都匹配不到。/(.*)\d/中的.\*可以匹配除了最后一位数字的所有字符,但是之前说的/.*/不是匹配了所有字符吗为什么后面的\d还可以匹配到一个数字字符?

首先我们要知道对于贪婪模式在进行匹配的时候会首先尝试匹配。意思就是/.*/匹配”abcd”的时候可以选择匹配a和不匹配a都是可以的,但是因为是贪婪模式所以选择了匹配a,b和c和d是同样的道理,到最后匹配完了abcd正则表达式匹配完成并且匹配成功。

对于懒惰模式在进行匹配的时候会首先尝试跳过。就是/.*?/匹配”abcd”字符串的时候首先尝试跳过a的匹配,再跳过b的匹配,直到最后正则表达式匹配完成,射门都没匹配到。

通过上面的描述可以看出贪婪和懒惰只是在每一个字符是否匹配上做的选择不同,相同的是在匹配和跳过的选择中正则表达式都会记住我在这里做了选择,这里还要其他选择。记住这些选择的作用就是当前选择如果走不通了,那么还可以回退到这里选择记录下来的另一条路,这就是回溯。 开始回溯的会选择离当前位置最近的一次选择,就是一个后入先出的栈的模式。

例1:/(.*)\d/匹配”abcd1”

第一步:因为.*是贪婪模式,所以会匹配字符a(并记住也可以不匹配a),往后匹配字符b(也可不匹配)一直往后匹配了字符c,字符d,字符1。

第二步:\d匹配的时候匹配不到字符,整个正则需要回溯,到选择是否匹配字符1的时候,之前选择了匹配现在要选择不匹配,让出了字符1。

第三步:\d匹配字符1,完成整个正则的匹配。

var reg = /(.*)\d/
var str = "abcd1"
var res = str.match(reg) //  ["abcd1", "abcd"] 可以看到分组(.*)匹配到了abcd并不包括1

例2:/(.*)\d/匹配”ab1cd”

第一步:因为.*是贪婪模式,所以会匹配字符a(并记住也可以不匹配a),往后匹配字符b(也可不匹配)一直往后匹配了字符c,字符d,字符1。

第二步:\d匹配的时候匹配不到字符,整个正则需要回溯,到选择是否匹配字符d的时候,之前选择了匹配现在要选择不匹配d,让出了字符d。

第三步:\d匹配字符d,匹配失败。继续回溯.*让出字符c。

第四步:\d继续匹配字符c,匹配失败。还要继续回溯.*让出字符1.

第五步:\d匹配字符1,完成整个正则的匹配。

var reg = /(.*)\d/
var str = "ab1cd"
var res = str.match(reg) //  ["ab1", "ab"] 可以看到分组(.*)匹配到了ab并不包括1

例3:/(.*?)\d/匹配”abcd1”

第一步:因为.*?是懒惰模式,所以不会匹配字符a(并记住可以匹配a)。

第二步:\d匹配字符a,匹配失败。回溯.*?重新选择匹配字符a,并继续放弃了字符b(记住可以匹配字符b)。

第二步:\d匹配字符b,匹配失败。回溯.*?重新选择匹配字符b,并继续放弃了字符c(记住可以匹配字符c)。

第三步:\d匹配字符c,匹配继续失败。继续回溯.*?重新选择匹配了字符c,继续并放弃了匹配字符d(记住可以匹配字符d)。

第四步:\d继续匹配字符d,匹配失败。还要继续回溯.*?重新选择匹配字符d,还是放弃了字符1(记住可以匹配字符1)。

第五步:\d匹配字符1,完成整个正则的匹配。

var reg = /(.*?)\d/
var str = "abcd1"
var res = str.match(reg) //  ["abcd1", "abcd"] 可以看到分组(.*)匹配到了abcd并不包括1

小结

对比例1和例3可以发现懒惰和贪婪模式匹配的结果是相同的,但是这并不意味着这两种匹配模式匹配结果是无差别的。对于两种模式匹配的字符串如果只有一个真确的匹配结果那么确是匹配得到的结果是一样的,但是一步一步检查过来就会知道虽然匹配结果一样但是经过的步骤是不同的。

如果匹配的结果不止一种可能,那么这两种模式匹配得到的结果就不一样了。例如,将字符串“abcd1”换成”abcd11”,那么这两种模式匹配到的结果就一样了。

var reg = /(.*?)\d/
var reg2 = /(.*)\d/
var str = "abcd11"

str.match(reg)  // ["abcd1", "abcd"]
str.match(reg2)  // ["abcd11", "abcd1"]

例4:/\w?(\w?)1/匹配“a1”

第一步:\w?匹配字符a(记住可以不匹配字符a)

第二步:(\w?)匹配字符1(记住可不匹配字符1)

第三步:1匹配不到任意字符,返回之前做选择的地方(\w?)从新选择放弃了字符1,什么都没有匹配

第四步:1匹配了字符1,完成整个正则表达式的匹配

var reg = /\w?(\w?)1/
var str = "a1"

str.match(reg) // ["a1", ""] 数组的第二个值也就是分组1(\w?)什么都没有匹配到

通过上面的步骤可以看出当需要回溯的时候会选择当前位置最近的一次选择,在重新开始,而不是从整个表达式的第一次选择开始重新选择。这是一个后进先出的模式就像栈一样。

断言/环视中的回溯

首先说结论,断言中的备用状态在断言匹配结束后会被丢弃,整个断言只能当做一个整体存在,回溯的时候不会进入断言中的备用状态。

例5:/(?=(.*))\11/匹配”11111”

这个正则什么都匹配不到。

第一步:(?=(.*))\1匹配了11111

第二步:1并不能匹配到任何字符串,并且准备回溯的时候发现前面并没有可回溯的状态,匹配失败

第三步:以为这就结束了吗?还没有会从第二个1再开始匹配,虽然得到的结果是一样的,直到最后一个1完成匹配,匹配失败,什么都没有匹配到

var reg = /(?=(.*))\11/
var str = "11111"

str.match(reg) // null

注:这是固化分组的一种模拟,固化分组指的是放弃分组中的备用状态,语法是(?…)。

分支中的回溯

首先多选分支是从左到右匹配的,并不会因为某个分支的或长或短就优先匹配,这样就会造成分支顺序不正确就导致某些分支永远不会被匹配到。

var reg = /abc|ab|abcd/
var str = "abcd"

str.match(reg) // ["abc"]

匹配结果是abc并不是ab,如果将分支调换位置那么得到的结果又将不一样。

var reg = /abcd|abc|ab/
var str = "abcd"

str.match(reg) // ["abcd"]
var reg = /ab|abcd|abc/
var str = "abcd"

str.match(reg) // ["ab"]

从上面的例子中可以看出多选分支的匹配是从左到右来选择分支来匹配的,并且在分支之间选择的时候会记住备用的选择,以备无法匹配的时候回溯到这里选择另一个分支。

例6:/((aa?)|(aa))a/匹配”aa”

第一步:选择分支(aa?)并且记住可以选择分支(aa),开始匹配字符aa

第二步:a匹配字符a

第三步:a? 匹配第二个a(记住可以不匹配这个a)

第四步:正则中最后一个a匹配的时候发现,无字符可匹配

第五步:回溯到第三步选择不匹配a字符

第六步:正则中最后一个a完成了匹配字符串中的a,整个正则匹配完成

var reg = /((aa?)|(aa))a/
var str = "aa"

str.match(reg) // ["aa", "a", "a", undefined]

例7:/((aa)|(aa?))a/匹配”aa”

第一步:选择分支(aa)并且记住可以选择分支(aa?),开始匹配字符aa

第二步:a匹配字符a

第三步:aa 匹配第二个a

第四步:正则中最后一个a匹配的时候发现,无字符可匹配

第五步:回溯到第一步选择分支(aa?)

第六步:这时就会重复例6的步骤,直到整个正则匹配成功

var reg = /((aa)|(aa?))a/
var str = "aa"

str.match(reg) // ["aa", "a", undefined, "a"]

注:双引号”表示的是字符串,并不是字符串的一部分

参考

精通正则表达式(第三版)

猜你喜欢

转载自blog.csdn.net/letterTiger/article/details/82464106