正則表達式原理及引擎簡化遞歸實現

轉載請註明作者:phylips@bmy 出處:http://duanple.blog.163.com/blog/static/7097176720098303134160/

 

概述

在正則表達式領域,有一本廣爲推崇的書籍<<精通正則表達式>>,但是作者在書中的很多地方假設那些匹配引擎採用的是回溯的算法。但是實際情況是有些引擎採用的NFA,DFA模擬算法,比如grep,awk,sed,對於它們來說算法複雜度是多項式級的。同時採用回溯的一些引擎也在逐步改進,比如通過採用備忘錄方法,記住已經回溯到達的狀態,防止重複,也可以避免指數級的複雜度。

所以實際上這本書中提到的很多優化方法,未來可能都是不必要的了。同時這本書對正則匹配引擎原理的介紹,也不過深入,只是一個概述性的,所以有些讓我失望,因爲當初買這本書的目的就是希望可以藉此深入正則匹配的內部原理。有鑑於此,所以纔有了這一篇文章的誕生。這篇文章主要參考了Brian W. Kernighan and Rob Pike對正則表達式的一個遞歸實現,以及Russ Cox 所寫的Regular Expression Matching Can Be Simple And Fast,這篇文章對grep的實現進行了更爲詳細的介紹。

實現一個正則匹配引擎,實際上就類似與實現一個簡單語言的編譯器。一個正則表達式就是用正則符號寫出的程序,我們要對這個式子進行語法分析,建立一個語法分析樹,根據這個樹生成NFA,如果採用NFA匹配的話,然後需要寫出NFA模擬執行的程序,用來進行匹配。而正則匹配引擎,本身與lex的實現很類似,所以基本上可以瞭解到詞法分析和語法分析的簡單內容。所以一方面我們可以瞭解正則匹配最深層的原理,另一方面也是對編譯原理的應用實踐。而且工作量始終,一個簡單的grep實現也就大概500c語言代碼。

當然設計時我們需要考慮這個正則匹配引擎的可擴展性,比如支持unicode,支持自定義的字符集合,可以方便添加一些新的運算,這樣下了,我們可以方便的逐步實現一個linux的grep擴展。另外可以實現一個java版本的,以代替指數級複雜度的實現。

首先我們來看一個簡化了的正則表達式,以及如何用遞歸回溯的方式,以最少的代碼實現它。

然後再來看一個grep的NFA實現,實際上它的算法早在1968年Ken Thompson, “Regular expression search algorithm,”中發表了。然而到了今天,很多語言(perl python pcre庫)的正則匹配引擎卻採用了一些更爲低級的算法,這樣的一些算法主要利用了回溯的方法,但是極端情況下卻會達到指數級的複雜度。採用這種回溯的方法可能是因爲實現者已經忘記了Ken Thompson的算法。另一方面,很多語言的正則表達式引入了前向引用(backreference)。這樣就使正則匹配超出了正則的範疇,這個情況下的匹配實際上是NP完全問題,一般也只能通過回溯解決,所以掌握遞歸回溯的方法也是一種需求。但是即使在這樣的匹配中,我們仍然可以把NFA作爲一種方案,在不出現backreference時使用NFA匹配。

同時我們可以看到,在grep採用的是最短匹配算法,這是與該應用本身相關的,因爲它的目的是搜索,但是對於一些以替換爲目的的正則匹配,則希望找到那個最長的匹配,這也是很多其他語言比如perl採用的默認選擇。

遞歸實現

一個簡化定義下的純遞歸實現,可以參考<<代碼之美>>第一章,只選擇了^.c*$這5個運算符

#include <stdio.h> 
#include <iostream> 
int match_star(int c,char *regexp,char*text);
int match_here(char *regexp,char*text){
    if(regexp[0] == '\0')return 1;
    if(regexp[1] == '*') return match_star(regexp[0],regexp+2,text);
    if(regexp[0] == '$') return text[0] == '\0';
    if(regexp[0] == '.' && text[0] != '\0')  return match_here(regexp+1,text+1);
    if(regexp[0] == text[0] && text[0] != '\0')  return match_here(regexp+1,text+1);
    return 0;

}

int match_star(int c,char *regexp,char*text){
    do{
        if(match_here(regexp,text) == 1) return 1;        
    }
    while((*text) != '\0' &&(*text++ == c || c == '.'));
    return 0;
}
int main() 

  char *a = "a*$";
  char *b = "aab";
  printf("%d",match_here(a,b));
  int printf = printf();
  cout << printf;
  getchar();
  return 0; 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章