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


本文介绍了一个简单的正则表达式引擎的实现,总共用了四百五十行左右的代码,完整的code可以在码云上找到。

基本的数据结构定义

核心思路是读取正则表达式以后生成对应的NFA,NFA中有边和状态两个结构。边的结构记录了它的起点和终点,同时通过枚举类型记录匹配的其他需求。

//用于处理‘^’字符
enum { NEXCLUDED = false, EXCLUDED = true }; 
//用于处理预处理类型,0-128以内ASCII字符直接匹配
enum { LCASES=256, UCASES=257, NUM=258, EPSILON=259, ANY=260, WS=261 };
class Edge
{
public:
    State *start;
    State *end;
    int type;
    int exclude;
    Edge(State *s, State *e, int t, bool ex = NEXCLUDED) :start(s), end(e), type(t), exclude(ex) {};
}

状态有预备,成功和失败三种,同时每个状态维护两个向量,向量存储了出边和入边的指针。

enum { READY = -1, SUCCESS = 1, FAIL = 0};
class State
{
public:
    int status;
    std::list<Edge *> InEdges;
    std::list<Edge *> OutEdges;
}

Nfa类会存储一个正则表达式,同时存储NFA的起点和终点,并使用了两个链表来维护NFA的边和状态,同时用一个链表来存储匹配成功的字符串。两个静态的字符串指针用于记录文件和正则表达式字符串的读取状态,静态常量,使得最终函数只会对文件内容和正则表达式扫描一次,避免在匹配成功的字符串中再匹配子串。

char *regex;
    State *Start;
    State *End;
    std::list<Edge *> edgeList;
    std::list<State *> stateList;
    std::list<char> matchedChar;
    static char *regRead;
    static char *fileRead;
}

生成NFA的过程中,通过currentEndcurrentStart两个指针分别指向当前字符读取完成后生成的最后一个状态和当前字符读取之前的开始状态,维护这两个指针的目的是为了记录NFA的生成过程,在处理‘*’、‘+’、‘?’等字符的时候起到了重要的作用。同时我们利用list内置的迭代器对链表进行遍历,这个方式在匹配过程中也用到了。

State *currentEnd, *currentStart;
State *alternate;
list<Edge *>::iterator itor;

NFA的生成

关键的部分在于匹配字符串时采取的思路,尤其是特殊字符的生成NFA的方式,这个不同于课本上最开始的NFA生成算法,而是基于读取字符串的过程,同时避免了字符串的回退等,读取一个字符就生成一个对应的边并压入链表中,对‘*’、‘+’,‘?’和特殊符号也是如此,使得处理更加简单的同时避免生成过于冗余的状态,兼顾了时间和空间效率。以下举例说明。

边和状态的生成

边的生成使用newEdge函数,需要记录起点和终点,以及类型,同时在生成边以后要用重载的两个patch函数将状态和边完全连接起来。

void Nfa::newEdge(State * start, State * end, int type, int exclude = NEXCLUDED)
{
    Edge *out = new Edge(start, end, type, exclude);
    end->patch(out, end);
    start->patch(start, out);
    edgeList.push_back(out);
}

以普通字符的生成和‘.’字符的产生方式为例,他们都是生成一条边和一个新的状态。

case '.':   /* any */
    currentStart = currentEnd;
    currentEnd = new State();
    newEdge(currentStart, currentEnd, ANY, NEXCLUDED);      
    stateList.push_back(currentEnd);
default:
    currentStart = currentEnd;
    currentEnd = new State();
    newEdge(currentStart, currentEnd, *regRead, NEXCLUDED);
    stateList.push_back(currentEnd);
    break;

如下图所示

create new edge
create new edge

接下来的符号处理都假定初始状态如下图所示

current stage
current stage

'|'的处理

currentStart指向的状态作为子NFA的起点,同时将子NFA的终点状态和原NFA的终点进行合并。

case '|':   // alternate 
    regRead++;
    currentStart = start;
    alternate= regex2nfa(regRead, start);
    currentEnd->merge(alternate);
    stateList.remove(alternate);
    regRead--;

如下图所示

spilt the edges
spilt the edges

'?' & '*' & '+'的处理

读取到问号只需要在上一条边的基础上继续连接原有的边即可。

case '?':   // zero or one 
    newEdge(currentStart, currentEnd, EPSILON, NEXCLUDED);
    break;

读取到''后,直接将currentStartcurrentEnd*进行合并成环

case '*':   // zero or more 
    alternate = currentEnd;
    currentStart->merge(alternate);
    stateList.remove(alternate);
    currentEnd = currentStart;
    break;

读取到‘+’后,只需添加若干条边从currentEnd状态指向currentStart状态的下一个状态即可。

case '+':   /* one or more */
    itor = currentStart->OutEdges.begin();
    for (;itor != currentStart->OutEdges.end();itor++)
        newEdge(currentEnd, (*itor)->end, (*itor)->type, (*itor)->exclude);
    break;

如下图所示:

special caracters
special caracters

简单的分组支持

对于中括号和括号进行了一定的支持,括号直接递归调用NFA的生成函数,中括号和预定义字符都有其对应的函数进行支持。

NFA匹配

匹配过程采用了递归的方式,step函数调用match函数匹配边和文件字符,匹配成功后即递归调用进入下一个状态。

if (End->status == SUCCESS) 
        return SUCCESS;

for(;itor != current->OutEdges.end();itor++)
{   
    if ((*itor)->match(fileRead))
    {
        (*itor)->end->status = SUCCESS;
        matchedChar.push_back(*fileRead);
        ++fileRead;
        if (step((*itor)->end))
            return SUCCESS;
            --fileRead;
        matchedChar.pop_back();
    }
    if ((*itor)->type == EPSILON && step((*itor)->end))
        return SUCCESS;
}
return FAIL;

结果

较好的通过了测试用例,但是没有进一步拓展功能,只是一个简单的正则表达式,同时也有些取巧,都只对字符进行了一次扫描,没有进行完整的词法分析和语法分析,程序在四百五十行左右的情况下,其实是不够健壮的。


routcome
routcome
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容