正则表达式的解析
正则表达式是如何解析的?
解析引擎的工作可以简单地分成两个部分:
1、生成状态机
2、执行状态机
状态机
状态机是什么呢?
状态机通常指有限状态机,是有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
举个例子:
一扇门,它有两个状态,【开着】和【关着】
在【开着】的状态下,进行【关门】的动作,门的状态就转移成【关着】
反之,在【关着】的状态下,进行【开门】的动作,门的状态就转移成【开着】
将这两个状态和它们的转换关系画成图:
![image-20230206171144603](http://image.huawei.com/tiny-lts/v1/images/e1c44ca09807768685454b9141d6e8e2_394x275.png)
这就是一个状态机。
分类
状态机有两种分类,确定性的与不确定性的,它们具体有什么区别呢?
还是以开门为例:
现在将门的状态更加地细化,不止分成【开着】、【关着】,还多了【开与关的中间态】。
同样地,对这个场景进行建模:
首先,我们定义一个状态为【半开/半关】
这个状态表示门开在中间
注意:这是一个状态。
![image-20230206173625865](http://image.huawei.com/tiny-lts/v1/images/060f06ec942ccfda8d6a69de79bdfa2b_669x467.png)
这样定义是容易理解的,因为我们对门这个东西有比较充分的了解,能够很直观地总结出这样三种状态。
但是,如果在一些创建一些没那么简单的状态机的时候,我们定义的状态很容易有重叠,在这个场景中,也可能将【半开/半关】定义为两个状态:【半开】与【半关】
如果我们基于此建立状态机:
![image-20230206175216952](http://image.huawei.com/tiny-lts/v1/images/b58966eba7317c722557b83df8117ce9_787x489.png)
有没有发现,最左边和右边的线上的动作是:什么都不做。
是的,在【半关】的状态上,什么都不做就可以转移到【半开的状态】,反之亦然。
上文提到:状态机有两个分类,确定性的与不确定性的。
在这里,第一个状态机是确定的,第二个状态机是不确定的。
为什么这么说呢?
在第一个状态机中,当处在某一个确定的状态中,如果动作是确定的,它的下一个状态也是确定的。
而在第二个状态机中则不是:
【开着】状态下,如果关一半,会进入【半关】状态,但是,有一个动作是什么都不做,进入【半开】状态。
也就是说,在【开着】状态下,如果关一半,可能进入【半关】也可能进入【半开】,进入的状态是不确定的。
执行
状态机的执行很简单:
输入有三个:起始状态、目标状态、一组动作。
还是回到门的例子里。
我的输入是:
起始状态是【关着】
目标状态是【开着】
动作组合是 开一半、开一半、关一半
执行状态机1:
![image-20230206180000370](http://image.huawei.com/tiny-lts/v1/images/c14b5ddb37a994f1b0fa3a3cfa7c1137_608x413.png)
最终的结果是【半开/半关】,没有到达预期的状态。
执行状态机2:
![image-20230206190841261](http://image.huawei.com/tiny-lts/v1/images/1342df7e38f9a6b6c410b1a36a58df94_764x535.png)
状态机2的执行有些不同的,当执行到第二个输入的时候,处于【半关】状态,此时有两个选择:
1、执行动作:开一半,进入【开着】状态
2、执行动作:什么都不做,进入【半开】状态
在这个例子中,最终是无法到达预期状态【开着】的。
如果选择1,发现失败,此时,需要回溯,选择2,试试看2是否能到达预期状态。
可以发现,回溯就是源自状态机的不确定性。对于状态机1来说,它执行的时候是不需要回溯的。
总的来说:
1、确定性状态机:对于任何动作,确定性状态机的执行都是确定的次数,等于它的动作数量。
2、不确定性状态机:执行的过程中需要回溯,与动作的数量不是一一对应的关系。
3、同时,每一个不确定的状态机都有一个确定的状态机与它等价
4、对于一种场景建模产生的状态机,确定性的只有一个,不确定性的可以有多个。
可以说,一个状态机的不确定性越强,同样的动作数量,其执行的次数越多。
ReDoS问题
有了上文的基础,可以给出一个简单的ReDoS问题的定义:
ReDoS问题,是正则表达式引擎在解析一些特殊的正则表达式的时候,生成的不确定性状态机过于复杂,回溯成本很高,导致执行时间的不可控产生的问题。
这里以一个常见的有ReDoS风险的正则表达式为例:
(a*)*
状态机:
![image-20230206193013572](http://image.huawei.com/tiny-lts/v1/images/ec7a2c3fb63b8b76448ed9efc4f11478_784x481.png)
起始是状态6,终止是状态4。
其中只有2->4这个状态对应非空的输入。
可以看到,这个状态机在字符串不匹配的情况下,回溯成本是非常高的。
如果正则表达式书写恰当,如:
a*
则生成的状态机就是:
![image-20230206193519530](http://image.huawei.com/tiny-lts/v1/images/fd3343d78757faf02be5a44f1474c4f9_628x376.png)
相对来说简洁了很多,执行成本也降低很多。
但是还有一个疑问:
为什么解析引擎不能将不恰当的正则表达式也生成一个恰当的状态机呢。
总的来说:
不恰当的正则表达式使解析引擎生成了不恰当的状态机,导致执行时耗时不可控。
那么,如何避免写出不恰当的正则表达式呢?它们有哪些特征呢?参考我的另一篇博文:
http://3ms.huawei.com/km/blogs/details/13615837?l=zh-cn
如果对解析引擎如何将正则表达式解析成状态机的过程感兴趣,可以参考:
http://3ms.huawei.com/km/blogs/details/13181693?l=zh-cn
哪些特征呢?参考我的另一篇博文:
http://3ms.huawei.com/km/blogs/details/13615837?l=zh-cn
如果对解析引擎如何将正则表达式解析成状态机的过程感兴趣,可以参考:
http://3ms.huawei.com/km/blogs/details/13181693?l=zh-cn