这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
学习正则表达式,是需要懂点匹配原理的。而在研究匹配原理时,也就必须得知道“回溯"了。回溯作为正则表达式中的高频词,听起来挺高大上,实际理解起来也并非想象中的困难。本篇将接上文正则式的表达,重点来讲解一下回溯,也希望现在还不清不楚的同学可以凭此文搞明白。
一、什么是回溯
回溯是正则表达式匹配过程的基础组成部分,却也会产生昂贵的计算消耗。当NFA自动机实现比较复杂的正则表述式时,匹配过程中可能会引起回溯问题。大量的回溯会长时间地占用CPU,从而带来系统性的开销。所以了解清楚回溯的基本工作原理,对于我们优化正则表达式是非常关键的。
1.1 回溯图解
那么 NFA 自动机到底是怎么进行匹配的呢?接下来以下面的例子来进行说明:
text = "abc"
regex = "ab{1,3}c"
上面的例子匹配比较简单,a开头,c结尾,中间1-3个b字符的字符串,所以回溯过程如下:
大家重点看第3步,明明b已经匹配了一个{1,3},为啥还又拉着匹配字母c再次匹配?这就是因为正则式表达的贪婪特性,所以b{1,3}会竭尽所能的匹配最多的字符。像以上去用正则表达式中的c去和文本中的c进行匹配的过程,就是发生了一次回溯。
非贪婪模式回溯过程:
独占模式回溯过程:
换个例子来进行说明:
text = "abbc"
regex = "ab{1,3}c"
我们直接从上面的例子从个细微修改,将文本该为abbc,正则不发生改变,回溯过程如下:
所以回溯的代价是比较大的,所以书写高效的正则表达式是非常重要的,否则可能会出现回溯失控的现象,导致我们系统的性能有所下降,降低用户的体验。
二、如何避免回溯问题?
既然回溯会给系统带来性能开销,那我们如何应对呢?如果你有仔细看上面那个案例的话,你会发现 NFA 自动机的贪婪特性就是导火索,这和正则表达式的匹配模式息息相关。
2.1 贪婪模式(Greedy)
顾名思义,就是在数量匹配中,如果单独使用 +、?、*或(min,max)等量词,正则表达式会匹配尽可能多的内容。
例如,上面那个例子:
text = "abbc"
regex = "ab{1,3}c"
就是在贪婪模式下,NFA自动机读取了最大的匹配范围,即匹配 3 个 b 字符。匹配发生了一次失败,就引起了一次回溯。如果匹配结果是“abbbc”,就会匹配成功。
text = "abbbc"
regex = "ab{1,3}c"
2.2 懒惰模式(Reluctant)
在该模式下,正则表达式会尽可能少地重复匹配字符,如果匹配成功,它会继续匹配剩余的字符串。
例如,上面的例子的字符后面加一个“?”,就可以开启懒惰模式。
text = "abc"
regex = "ab{1,3}?c"
匹配结果是“abc”,该模式下 NFA 自动机首先选择最小的匹配范围,即匹配 1 个 b 字符,因此就避免了回溯问题。
2.3 独占模式(Possessive)
同贪婪模式一样,独占模式一样会最大限度地匹配更多内容;不同的是,在独占模式下,匹配失败就会结束匹配,不会发生回溯问题。
还是上面的例子,在字符后面加一个“+”,就可以开启独占模式。
text = "abbc"
regex = "ab{1,3}+c"
结果是不匹配,结束匹配,不会发生回溯问题。
所以综上所述,避免回溯的方法就是:使用懒惰模式或独占模式。
三、如何避免回溯问题?
**1.少用贪婪模式:**多用贪婪模式会引起回溯问题,可以使用独占模式来避免回溯。
**2.减少分支选择:**分支选择类型 “(X|Y|Z)” 的正则表达式会降低性能,在开发的时候要尽量减少使用。
**3.减少捕获嵌套 :**捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组中,方便后面引用。一般一个()就是一个捕获组,捕获组可以进行嵌套。