介绍
这个篇文章是对我的一个基本功能的正则引擎的实现过程的大致介绍与讨论,实际上在之前的文章中我已经对正则引擎的实现有所涉及,当时是翻译了有关Thompson NFA的文章,文章及其附带的源代码中已经对如何实现一个简单的正则引擎有了详细的说明,但是该文章对正则引擎的介绍并不深入,同时实现方式也过于简单化,虽然最终的结果是高效可靠的,但是这个实现不便于扩展,同时支持的功能也比较有限,因此在这一次的实现中,我采用了更加主流的方法,这样支持了更多的功能,同时也易于扩展。该项目使用的语言为C++,完整项目请移步regexEngine2。
引擎功能介绍
下面简单地介绍一下这一次的正则的一些功能(并联串联功能不再赘述):
1.’*’:零个或多个
2.’+’:一个或多个
3.’?’:零个或一个
4.’ . ‘:匹配任意一个字符
5.{a}:重复a次;{a,b}:重复次数大于等于a小于等于b;{a,}:重复次数大于等于a
6.[m-n]:匹配范围内的字符(例如:[a-g]等价于a|b|c|d|e|f|g等价于[abcdefg])
7.[^m-n]:匹配不是范围内的字符
同时还加入了一些特殊的转义字符:
1.\d:等价于[0-9]
2.\D:等价于[^0-9]
3.\s:等价于[\t\n\r\f]
4.\S:等价于[^\t\n\r\f]
5.\w:等价于[a-zA-Z0-9_]
6.\W:等价于[^a-zA-Z0-9_]
由此可见,这一次的正则引擎的功能相较于之前的实现有了更多的加强,这样一来使得正则匹配更加方便易用,但就如同我刚开始说的,这也仅仅是一个基本的功能,因为真正工业级别的正则引擎还有许多高级但是实用的功能,例如:贪婪匹配与非贪婪匹配(其实这个实现起来比较简单),正向预查,反向预查等等,这些功能使得正则表达式更加强大,但是实现起来也更加的复杂,由于我的本意并不是做一个完备的正则引擎,因此这些高大上的功能就跳过。下图大致展示了程序匹配时的情况:
图中的match方法的第二个参数为匹配的模式,第一种是ALL_MATCH,第二种是SUB_MATCH,前者将整个给定的字符串作为输入进行匹配,后者会将给定字符串中与表达式匹配的地方筛选出,并存储到result属性中。
同时这一次的实现我也加入了对宽字符的支持:
最后当书写的正则表达式出现错误时,错误的提示也会更加详细(有一个背景反色的提示,并没有什么用,正儿八经的正则引擎不会搞这种东西出来,只是好玩儿而已):
既然正则引擎已经实现了,那顺便实现一个词法分析器的模块也是比较轻松的事情,在头文件analyzer.h文件中定义了两个类,一个是MatchUnit,一个是LexicalAnalyzer,在使用词法分析器的时候,需要实例化MatchUnit类,之后使用MatchUnit的实例来初始化LexicalAnalyzer,在设定好所需要分析的文件之后,便可以通过调用get_next_token(命名方式有点乱)方法获取词法单元。
实现
这一次引擎的实现我采用了比较主流的方法,即首先对正则表达式进行解析,形成抽象语法树,通过抽象语法树得到表达式的字符集以及NFA,通过子集构造法将NFA转换为DFA,最后通过将DFA中的相同状态合并,得到最小化的DFA,这样得到的状态转换表便最终用来进行字符串的匹配。实际上之前的Thompson NFA的实现方法是将正则表达式转换为NFA,之后使用该NFA进行匹配,这种方法实现简单,但是当正则表达式很复杂时,生成的NFA的状态是很多的,这样匹配速度会下降,特别是在重复进行匹配时,时间的消耗很大。而最小化后的DFA状态转换相较于NFA会少了很多的不必要的状态,这样匹配速度会得到大幅提升。接下来我会分别对上述的几个处理过程进行介绍。
解析正则表达式
实现的第一步是解析正则表达式,而解析的时候文法是必不可少的,以下是我从网上找到的正则表达式的文法:
RE::= union | simple-RE
union::= RE "|" simple-RE
simple-RE::= concatenation | basic-RE
concatenation::= simple-RE basic-RE
basic-RE::= star | plus | ques | elementary-RE
star::= elementary-RE "*"
plus::= elementary-RE "+"
ques::= elementary-RE "?"
elementary-RE::= group | any | eos | char | set
group::= "(" RE ")"
any::= "."
eos::= "$" //end of string
char::= any-non-metacharacter | "\" metacharacter
set::= positive-set | negative-set
positive-set::= "[" set-items "]"
negative-set::= "[^" set-items "]"
set-items::= set-item | set-item set-items
set-item::= char-range | char
char-range::= char "-" char