基于ε-NFA的正则表达式引擎

正则表达式几乎每个程序员都会用到,对于这么常见的一个语言,有没有想过怎么去实现一个呢?乍想一下,也许觉得困难,实际上实现一个正则表达式的引擎并没有想像中的复杂,《编译原理》一书中有一章专门讲解了怎么基于状态机来构建基本的正则表达式引擎,讲这个初衷是为词法分析服务,不过书里的东西相对偏理论了些,实现起来还是要费些功夫的,只是它到底指明了一条路,当然,书里只针对基本的语法进行了分析讲解,对于在实际中有些非常有用的很多扩展语法,它就基本没有涉及了,这些扩展的语法中有些是比较好实现的,有些则很难。

基本的正则表达式

正则表达式由字符与元字符组成,整个表达式用于描述符合某些特征的字符串,比如说:abc,表示“abc"这个字符串,由‘a‘,‘b‘,‘c‘三个字符按顺序连接在一起。
基本的正则表达比较简单,主要包括以下操作符及元字符(meta-character):

  • 连接符,该操作符没有对应的符号表示,如上述abc,我们默认a与b,b与c之间有一个连接符。

  • 或操作符,由‘|‘表示,该操作符将它左右两边的正则表达式是一个或的关系,待匹配的字符只要符合其中的一个,就是符合条件的。

  • 重复操作符,用三个:分别是‘+‘,‘*‘,‘?‘,分别用于表示将前面的正则表达式单元重复至少一次,至少0次,0次或1次。

  • 集合,用‘[]‘围起来,表示符合的字符。

  • 任意字符,用‘.‘表示,该字符表示匹配任意字符。

  • 单元或者说组,用括号‘(‘,‘)‘表示,该字符用将一组正则表达式当成一个单元,使得其它的操作将该单元作为一个整体,比如说(ab)+表示重复"ab“至少一次。

上述描述的操作符有优先级的区别,最弱的是或操作符,其次是连接,最后是重复操作符,比如abc|efg,就等价于(abc)|(efg)。

扩展的正则表达式

由前面的说明,我们可以发现基本的正则表达式相对来说是比较弱的,语法上也很简单,容易实现的同时不可避免地相对功能就偏弱,于是就有了扩展的语法,扩展的语法相对就复杂了,我就不一一介绍,具体可以参考维基百科上的描述,对于本文来说,主要想实现其中的如下几个语法:

  • 重复,用{min,max}表示,该语法表示将前面的单元重复min到max次,是个闭区间。

  • 头和尾,分别用‘^‘,‘$‘表示,表示字符串以该正则表达式描述的样子开关,和结尾。

  • 向后引用(back
    reference),用\1,\2等反斜杠加数字表示,这些符号表示引用前面已经匹配好的内容,如([ab]cc)cd\1,其中的\1在实现匹配时就会等于前面括号里的表达式匹配到的内容。

加入这几个语法,主要是因为它们太常用也太有用了,前面两个也还好实现,向后引用这个却是很麻烦的,而且实现起来效率很低,后面会介绍。

ε-NFA

实现正则表达式引擎,目前来说流行的做法主要有两种,一种是各大语言里(perl,
python,etc)常用的回溯法(backtracking),一种是龙书里说的基于状态机的做法。二者的实现各有优劣,回溯法相对来说实现功能较容易,但算法效率很低,状态机的实现,最大的优点是效率很高,但对于扩展的语法实现起来比较困难,而且代码相对不好理解。
对于基本的正则表达式语法来说,用状态机实现是很理想的,性能很高,而且比较容易实现,龙书里所说的ε-NFA(non-deterministic finite
automata)是这样一种状态机,首先就是某些状态对同一个输入,它可以有多个不同的转换,然后就是除了一般状态机所具有的状态与具体转换之外,还加入了一种叫作ε的状态及ε转变:

如上图所示,状态3就是我们所说的ε状态,该状态只能通过ε转换从别的状态转过来,也只能通过ε转换变转到其它状态,其中,ε转换指的是不需要任务输入就可以进行的转换。ε状态与ε转换的加入让状态机的构建更加容易与清晰,同时在某些情况下也使得一些特殊功能更加好实现,但是ε状态过多也是有坏处的,它使得状态机的状态转变复杂变冗余了,因此应该尽可能的少用。

从正则表达式到ε-NFA

一条完整的正则表达式可以看成是一系列小的正则表达式的组合,这些组合的关系根据前面的介绍主要可以概括为如下几种:

  • 单个字符,这是正则表达式的基本单元,如‘a‘,‘b‘,’c‘等。

  • 连接(concat),表示将两个正则表达式连接起来,是一个并的关系。

  • 或组合,表示两个正则表达式由‘|‘连接起来。

  • 重复,表示将前面的正则表达式重复指定的次数,如:?,+,*, {2, 4}等。

将正则表达式转换为ε-NFA的原则就是先从小的正则表达式开始,先将单个字符转为各个小的ε-NFA,再将这些ε-NFA根据组合关系拼凑成完整的ε-NFA。

对于单个字符来说,它的ε-NFA很简单,只有两个状态,一个转换:

concat组合则主要是将两个ε-NFA用ε转换连起来:

接下来是或组合:

对于重复组合来说,情况稍微复杂,其中‘?‘, ‘+‘, ‘*‘,我们只需要在子ε-NFA的开始与结束状态之间加入ε转换则可,如下所示:


重复一次或0次


重复至少一次


重复任意次

对于扩展语法中的指定重复次数,我们可以采取将状态直接复制的做法,比较暴力,但管用,如:(a){2, 4},我们得到如下ε-NFA

注意其中后三组不同颜色的状态,它们是从第一组状态复制过来的。
扩展的语法里,还包括如:{0,≌}这样的重复,我们只要把状态按最小的重复次数复制一遍,然后像+,*样加ε转换就行了:如{2,≌}

正则表达式的语法树

前面描述了怎么将小的ε-NFA组合成大的ε-NFA,我们知道,关键是先从小的正则表达式开始,但是具体在面对正则表达式时,我们怎么把一条完整的正则表达拆成小的正则表达式呢?
为了将大正则拆成小的正则,我们可以借助语法树的帮助,所谓的语法树在这里是指这样的一棵树,它的内部结点是操作符,内部节点的子树则是该操作符的操作数,而叶结点则是具体的符号,在这里操作符只有三种:或(or),
concat, 重复(统一用star表示), ,如:我们可以将(ab)+cd(e|f)转换为如下一棵语法树:

显然对于任意一个内部结点来说,它的左右子树,就分别代表了一个小的正则表达式,而叶子结点则是最小的,解释这样一棵树显然简单多了,至于怎样构建语法树,仔细想想,在正则表达式里,表达式与操作符是右结合的,如:a+,
然后两个表达式之间要么是是concat组合,要么是或组合,所以我们在构造语法树时,可以考虑从右往左,依次将各个小的表达式抽出来,然后对该小的正则表达式构建语法树则可。

```cpp

TreeNode* ConstructSynTree(const char* regstart, const char*
reg
end)

{

 const char* right_exp = ExtractExpression(reg_start, reg_end);

int operator = GetOperator(right_exp - 1);

TreeNode* node = CreateInteriorNode(operator);

TreeNode* left_child = ConstructSynTree(reg_start, right_exp - 2);

TreeNode* right_child = ConstructSynTree(right_exp, reg_end);

node->left = left_child;

node->right = right_child;

return node;


}

```

详细代码可以参考这里

部分扩展语法的实现

前面讲的内容主要是针对基本的正则表达式语法,原理主要来自龙书的介绍,只是在实现上我尽可能减少了ε状态,因为没有涉及扩展语法,前面的算法写起来是很简单的,大概只需要一千行左右代码就可以写出来,而且效率是很高的,只是因为太简单使用起来不方便,只能玩玩,下面我讲一下怎么实现前面提到几个扩展语法。

首先是关于重复,这个比较简单,前面已经讲了,至于匹配头(^),匹配尾($),这个实现也比较容易,只要分加一个ε状态,对于匹配头时,该状态只有一个向外的ε转换,对于匹配尾时,该ε状态对任何输入都转换为自己则可:

而至于向后引用,这个语法在现实中是很实用的,因此我才想着要把它加进来,但等到真正实现时,才发现这个功能却出乎意料的难以实现,根据这篇文档的介绍,正则表达式中向后引用的实现是一个NP完全问题,到目前来说,还没有发现高效的实现方法,而我面对的问题已经不是高效不高效的问题,而是在一个简单的ε-NFA状态机框架上要加入这个功能都是比较痛苦的,至于我现在的实现,已经把原先的状态机给hack了才做出来,代码也写得很难看了,这个以后得再想想能不能把实现设计的好一点。

要想实现这个向后引用,关键在于及时把前面括号里的正则表达式所捕获的内容保存下来,而一般来说,状态机的状态本身应该是没有状态的,它不应该记住它在前一个状态做了什么事情,这些限制都让实现很为难。
但是为了捕获括号里的正则表达式所匹配的内容,我们又必须清楚地知道,当前状态机是否进入了某个括号的正则表达式里,以及什么时候退出了该括号,为达到这个效果,我们在构建状态机时,可以引入两个特殊的ε状态,其中一个状态称作ε-unit-start,用来表示,下次输入如果导致当前状态发生转换,则需要开始保存后续的输入,另一个状态称作ε-unit-end,用来表示,如果进入了该状态,则如果后续输入导致该状态被转换出去,则应该停止保存后续的输入。

想法是这样,但实现起来有很多细节需要注意,因为是ε-NFA,对于每一个输入,状态机可能会得到好多个新的状态,因此:

  • 有时我们可能在同一时间进入ε-unit-start和ε-unit-end。

  • 有时可能好几个ε-unit-start与ε-unit-start同时出现。

  • 有时还没有进入ε-unit-start, 却发现先进入ε-unit-end了。

  • 甚至有时进入ε-unit-start后,却发现永远都不会进入对应的ε-unit-end了。

这些都需要一一处理好,特别是类似(a),(a), a(cd)*fe这种表达式,括号里可能捕获空内容的情形。

我实现基本的正则表达式,只花了二三天时间,但为了使现这个向后引用,却反复修改,二三个星期才搞好。。。

现在代码差不多写好了,有兴趣的可以去瞄瞄,先从unit
test看起,代码确实可读性有些差,做好心理准备就是了。

基于ε-NFA的正则表达式引擎,布布扣,bubuko.com

时间: 2024-10-08 11:37:51

基于ε-NFA的正则表达式引擎的相关文章

1000行代码徒手写正则表达式引擎【1】--JAVA中正则表达式的使用

简介: 本文是系列博客的第一篇,主要讲解和分析正则表达式规则以及JAVA中原生正则表达式引擎的使用.在后续的文章中会涉及基于NFA的正则表达式引擎内部的工作原理,并在此基础上用1000行左右的JAVA代码,实现一个支持常用功能的正则表达式引擎.它支持贪婪匹配和懒惰匹配:支持零宽度字符(如"\b", "\B"):支持常用字符集(如"\d", "\s"等):支持自定义字符集("[a-f]","[^b-

简易正则表达式引擎的实现

正则表达式基本每个程序员都会用到,实现正则表达式引擎却似乎是一个很难的任务.实际上,掌握<编译原理>前端的词法分析部分知识就能够实现一个简单的正则表达式引擎.这里推荐一下网易云课堂的课程.http://mooc.study.163.com/course/USTC-1000002001?tid=1000003000#/info 基本的正则表达式  正则表达式由字符与元字符组成,整个表达式用于描述符合某些特定特征的一类字符串,比如说表达式:abc,它表示 "abc" 这个字符串

实现一个 DFA 正则表达式引擎 - 4. DFA 的最小化

(正则引擎已完成,Github) 最小化 DFA 是引擎中另外一个略繁琐的点(第一个是构建语法树). 基本思路是,先对 DFA 进行重命名,然后引入一个拒绝态 0,定义所有状态经过非接受字符转到状态 0,0 接受所有字符转换为自身.也就是说我们先建立一个转换表,然后把第一行填写为: state a b c d e f g h ... 0 0 0 0 0 0 0 0 0 0 再之后,我们讲 DFA 的其余状态从 1 开始重命名,填入状态表.代码实现如下: // rename all states

(2015大作业)茹何优雅的手写正则表达式引擎(regular expression engine

貌似刚开学的时候装了个逼,和老师立了个flag说我要写个正则表达式引擎,然后学期末估计老师早就忘了这茬了,在历时3个月的懒癌发作下,终于在这学期末deadline的时候花了一个下午加晚上在没有网的房间写完了它,于是便有了这篇blog,本来想正儿八紧写篇论文,说不定毕业设计可以直接丢一篇这个走人,但第一觉得一个晚上写好的东西太low了,第二自己实在不适合写那种正经的论文,于是还是写从高中开始的一贯的乱七八糟体好了. 主要写自己写的时候遇到的一些瓶颈,例如茹何储存一个图,茹何遍历一个图,茹何表示一个

Lucene:基于Java的全文检索引擎简介 (zhuan)

http://www.chedong.com/tech/lucene.html ********************************************** Lucene是一个基于Java的全文索引工具包. 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史 全文检索的实现:Luene全文索引和数据库索引的比较 中文切分词机制简介:基于词库和自动切分词算法的比较 具体的安装和使用简介:系统结构介绍和演示 Hacking Lucene:简化的查询分析器,删除的

基于 CoreText 的排版引擎

本章前言 使用 CoreText 技术,我们可以对富文本进行复杂的排版.经过一些简单的扩展,我们还可以实现对于图片,链接的点击效果.CoreText 技术相对于 UIWebView,有着更少的内存占用,以及可以在后台渲染的优点,非常适合用于内容的排版工作. 本章我们将从最基本的开始,一步一步完成一个支持图文混排.支持图片和链接点击的排版引擎. CoreText 简介 CoreText 是用于处理文字和字体的底层技术.它直接和 Core Graphics(又被称为 Quartz)打交道.Quart

Lucene:基于Java的全文检索引擎简介

Lucene是一个基于Java的全文索引工具包. 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史 全文检索的实现:Luene全文索引和数据库索引的比较 中文切分词机制简介:基于词库和自动切分词算法的比较 具体的安装和使用简介:系统结构介绍和演示 Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展 从Lucene我们还可以学到什么 另外,如果是在选择全文引擎,现在也许是试试 Sphinx的时候了:相比Lucene速度更快, 有中文分词的

基于 CoreText 的排版引擎:基础

本文节选自我的图书:<iOS 开发进阶>. 本文涉及的 Demo 工程在这里:https://github.com/tangqiaoboy/iOS-Pro. 扫码关注我的「iOS 开发」微信公众帐号: 本章前言 使用 CoreText 技术,我们可以对富文本进行复杂的排版.经过一些简单的扩展,我们还可以实现对于图片,链接的点击效果.CoreText 技术相对于 UIWebView,有着更少的内存占用,以及可以在后台渲染的优点,非常适合用于内容的排版工作. 本章我们将从最基本的开始,一步一步完成

基于 CoreText 的排版引擎:进阶

版权说明 原创文章,转载请保留以下信息: 本文节选自我的图书:<iOS 开发进阶>. 本文涉及的 Demo 工程在这里:https://github.com/tangqiaoboy/iOS-Pro. 扫码关注我的「iOS 开发」微信公众帐号: 本章前言 在上一篇<基于 CoreText 的排版引擎:基础>中,我们学会了排版的基础知识,现在我们来增加复杂性,让我们的排版引擎支持图片和链接的点击. 支持图文混排的排版引擎 改造模版文件 下面我们来进一步改造,让排版引擎支持对于图片的排版