人生第一篇技术类博文=.= 因为工作需要,自己写了个支持多语种的正则表达式解析器,记录下心得。
概述
概括的来说,正则表达式的识别包含如下步骤:
确定语法>>构造语法树>>构造自动机>>优化
如果只是做一个简单的识别程序,可以省去第一和最后一步,略过第一和第四章。
1.语法
语法是一个正则表达式的核心,也是唯一需要完全靠自己发挥的步骤。以我的实际程序为例,我的语法构造步骤如下:
1.1 确定运算符和优先级,例如
| 运算符 |
含义 |
说明 |
优先级 |
| * |
表示前一个字符重复零次或多次 |
a* = a; aa; aaa; aaaa... |
3 |
| | |
表示或的关系 |
a|b = a 或者 b |
2 |
| SPACE |
连接符 |
|
1 |
| () |
括号 |
通常用于改变运算顺序 |
N/A |
其中 * | SPACE 在编译原理书中有专门的命名,依次为Kleen Star, Union, Concate。在后面的章节可以发现很多运算符都能转换成这三个运算符的组合。
1.2 根据运算符确定初始的语法
简单的来说,就是告诉程序分解正则表达式的方法。比如说按照1.1的运算符,有正则表达式:
(a|b)* a b b (表达式1)
那么程序就应该这样处理:先把空格隔开的字符分别拿出来:(a|b)*、a、b和b。最后三项已经是单个字符放一边,再看第一项,依次去掉星号、括号、脱去|,最后也成为单个字符,然后进行解析。这边我直接给出我的初始语法:
Gramma 1:
regex -> exp CONCATE regex | exp
exp -> state UNION exp | state
state -> token | token KLEENSTAR
token -> (regex) | 单个字符
语法设计的主要依据是运算符及其优先级,一般来说是为每个优先级设定一个转换规则。上例中从上到下优先级分别为1、2、3、N/A。
1.3 语法的完善与优化
一般情况下,根据上一步得到的初始语法就能直接用于构造语法树。但是在较为复杂的应用中,我们还需要检查语法是否存在left-factor和left-recursion(这里不讨论歧义问题)。例如:
Gramma 2:
A -> Aa | b
为直接的left-recursion语法,即A在多次转换后,其结果的最左项可能依然为A。对于解析顺序为从左到右的解析器来说,这样的语法将不利于程序判断什么时候应该停止继续转换。简单的消除办法为添加中间结果项,对于语法2可得:
Gramma 3:
A -> bA'
A' -> epsilon | aA' (这里epsilon 表示空字符串)
更为复杂的情况为间接的left-recursion,具体定义和解决算法可以参照龙书4.3节。对于正则表达式解析,通过良好的设计可以避免这种情况。
Gramma 4:
A -> a | ab | abc
语法4为一个left-factor语法,即某个转换的两个或多个结果具有相同的最左项,这不利于解析器确定该转换应该采用的结果。解决的方法类似于合并同类项,对于上例可以得到:
Gramma 5:
A -> aB
B -> epsilon | bC
C -> epsilon | c
2.构造语法树
在确定语法之后,就可以根据语法来构造语法树,构造的方法有多种,不过关键都在于构造转换表,这部分内容是编译原理的核心部分,可以参照龙书第四章。我采用的是LL(1)的解析方法,即从左到右,从顶向下的解析顺序,并且在解析过程中根据当前解析器状态和当前输入字符可以唯一确定一个转换结果,这个方法的关键在于构建一个状态转换表。对于这种方法,请务必确认已经消除left-recursion和left-factor。表达式1:(a|b)* a b b的构造结果如下,其中#是用于表示字符串结束,无实际意义

3.构造自动机
在构造完语法树之后,就可以构造自动状态机了。共有两种方式:DFA和NFA。
3.1 NFA
NFA的构造方式比较简单,空间复杂度较低,但匹配的时间复杂度较高。
对于上一张的语法树来说,每一个树节点都对应着一个NFA,NFA的结构由该节点的运算符/字符决定,开始/结束状态则由子女节点决定。例如:
对于s UNION t,按照以下的方式生成NFA N(s|t)。

对于s CONCATE t,按照以下的方式生成NFA N(s t)。

对于s KLENNSTAR,按照以下的方式生成NFA N(s*)。

这个过程从叶节点开始,一直到根节点结束,最后根节点得到的NFA就是我们所需的NFA。
3.2 DFA
相比于NFA,DFA空间复杂度更大,但匹配的时间复杂度较小,因此在实际应用中通常采用该方法。
DFA可以从NFA得到,要点在于:将NFA中的每一个状态集转换为DFA中的一个状态。
另外一种是从语法树直接生成DFA,d