原創 正則引擎完工,記錄下思路和設計

IceCrystals發表於2014-10-26

最近20天都在寫這個...終於完工了(走向無盡的重構道路...)...感謝VC聚聚的博文和RE2作者的部落格指導,感謝VC聚聚的原始碼參考.非常感謝!啟發很大.vc聚聚的正則語法樹遍歷部分的方案.真是精妙!之前我雖然知道用Visitor模式遍歷異構樹,但是不知道怎麼寫vistor的框架滿足需求.用的時候不斷地感嘆設計的好.不過我也就抄了這塊框架程式碼:)因為實現的太好了.其他都是根據博文給的參考設計自己去想設計和實現

整個引擎實現了http://blog.csdn.net/lxcnn/article/details/4268033提到的全部功能,另外新增了VC聚聚引擎中包含的命名子表示式的功能.在有些場合可以簡化正規表示式長度.書寫更加方便.

高層設計:

  基本上來說正則引擎的實現需要完成:

  詞法分析

  語法分析

  字符集合正交化

  構建NFA

  根據NFA的邊型別將NFA的不同部分分解,能構建DFA的構建DFA

  構建Regex解析類 包括正則的DFA和NFA的實現.

  寫出正則的匹配演算法,在正則匹配的不同階段,切換DFA和NFA匹配過程.

模組設計:
正則語言的詞法分析部分是很簡單的,整個都可以用字串匹配匹配出來,不用構建DFA之類的去做.
具體要解析的token如下文法中的""包括的內容
Alert = Unit "|" Alert
Unit;
Unit = Express Unit | Express
Express = Factor Loop | Factor
Loop = “{” Number “}”
= “{” Number “,” “}”
= “{”Number “,” Number “}”
= “{” Number “}?”
= “{” Number “,” “}?”
= “{”Number “,” Number “}?”
= "*"
= "?"
= "+"
= "*?"
= "??"
= "+?"
Factor = “(” Alert “)”
= “(<” Name ">" Alert “)”
= "(?:" Alert ")"
= "(?=" Alert ")"
= "(?!" Alert ")"
= "(?<" Alert ")"
= "(?<!"Alert ")"
= "$"
= "^"
= Backreference
= CharSet
= NormalChar
CharSet = "[^" CharSetCompnent "]" |"[" CharSetCompnent "]" | Char | "\X"
CharSetCompnent = CharUnit CharSetCompnent | CharUnit
CharUnit = Char "-" Char | Char
Backreference = "\k" <” Name ">" | "\" Number
Note = "(#" ... ")"
詞法分析:
整個解析的框架是:

Ptr<vector<RegexToken>> RegexLex::ParsingPattern(int start_index, int end_index)
{
Ptr<vector<RegexToken>> result(make_shared<vector<RegexToken>>());
for(auto index = start_index; index < end_index;)
{
for(auto catch_length = 4; catch_length >= 1; catch_length--)
{
auto&& key = pattern.substr(index, catch_length);
if(RegexLex::action_map.find(key) != RegexLex::action_map.end())
{
//RegexLex::action_map執行完後,index指向正確的位置了.也就不用++了.
RegexLex::action_map[key](pattern, index, result, optional);
break;
}

if(catch_length == 1)
{
//說明是普通字元. normal長度5 :)
RegexLex::action_map[L"normal"](pattern, index, result, optional);
}
}
}
return move(result);
}

 

action_map的key就是token的字串形式,返回的是一個enum token表明符號型別.
optional是.NET正則匹配時候的可選選項的內容.
詞法分析的結果是一個RegexToken型別的vector,
class RegexToken
{
public:
TokenType type;
CharRange position;
}
包含了token型別和發現的位置區間.
之後vector作為語法分析器的輸入和原始的模式串一起進行語法分析.(根據token的位置去模式串裡面找需要的資訊);
詞法分析部分需要注意的地方:
1.[]內的-左右是[或者]時候,當做普通字元看待.[]內的字元除了無轉義的]都當做普通字元看待.
2.(?=)+LookAround的)後面的表達重複的元字元當做普通字元看待.
3.要考慮到表示式巢狀.處理的時候遇到(XXX這種表示一個子表示式開始的字元,要先用棧找到匹配的結尾的")" ( ")"可能和多個元字元匹配成子表示式).例如(12(321))解析外層括號的時候,遇到"("需要正確處理它的結尾是最後一個")";
語法分析部分:
語法分析部分根據龍書地語法分析教程寫的.之前輪yacc的時候寫過了LALR語法分析器,所以這回換換口味,寫了個LL的語法分析器.具體的做法參考龍書的LL語法分析器部分.整個語法分析部分的文法是:

文法:

Alert = Unit "Alternation" Alert | Unit
Unit = Express Unit | Express
Express = Factor Loop | Factor
Loop = “LoopBegin” |"ChoseLoop" | "ChoseLoopGreedy" | "PositiveLoop" | "PositiveLoopGreedy" | "KleeneLoop" | "KleeneLoopGreedy";

Factor
= “CaptureBegin” CaptureRight
= "AnonymityCaptureBegin" AnonymityCaptureRight
= "RegexMacro" CaptureRight
= "NoneCapture" Alert "CaptureEnd"
= "PositivetiveLookahead" Alert "CaptureEnd"
= "NegativeLookahead" Alert "CaptureEnd"
= "PositiveLookbehind" Alert "CaptureEnd"
= "NegativeLookbehind" Alert "CaptureEnd"
= "StringHead"
= "StringTail"
= "Backreference"
= "CharSet"
= "NormalChar"
= "LineBegin"
= "LineEnd"
= "MatchAllSymbol"
= "GeneralMatch"
= "MacroReference"
= "AnonymityBackReference"
CaptureRight = "Named" Alert "CaptureEnd" |Alert "CaptureEnd"
""包含的是詞法分析過程中返回的正則的token.語法分析的難點....額..和手寫LL語法分析器的難點差不多.首先要構造出first表.反正也不是寫yacc=.=...我就人腦構造first表了.之後根據文法寫出LL分析器.返回一個語法樹.
語法樹的節點型別:

class Expression :public enable_shared_from_this < Expression >
{
public:
virtual void Apply(IRegexAlogrithm& algorithm) = 0;
bool IsEqual(Ptr<Expression>& target);
Ptr<vector<CharRange>> GetCharSetTable(const Ptr<vector<RegexControl>>& optional);
void SetTreeCharSetOrthogonal(Ptr<CharTable>& target);
pair<State*, State*> BuildNFA(AutoMachine* target);
private:
void BuildOrthogonal(Ptr<vector<int>>&target);
};

//字符集合
class CharSetExpression : public Expression
{
public:
bool reverse;
vector<CharRange> range;
public:
};
//普通字元
class NormalCharExpression : public Expression
{
public:
CharRange range;
};

//迴圈
class LoopExpression : public Expression
{
public:
Ptr<Expression>    expression;
int begin;
int end;
bool greedy;
public:
};

class SequenceExpression : public Expression
{
public:
Ptr<Expression> left;
Ptr<Expression>    right;
public:
};

class AlternationExpression : public Expression
{
public:
Ptr<Expression> left;
Ptr<Expression>    right;
};

class BeginExpression : public Expression
{
public:
};

class EndExpression : public Expression
{
};

class CaptureExpression : public Expression
{
public:
wstring    name;
Ptr<Expression> expression;
};
class AnonymityCaptureExpression : public Expression
{
public:
int    index = 0;
Ptr<Expression> expression;
};

class MacroExpression : public Expression
{
public:
wstring    name;
Ptr<Expression> expression;
};
class MacroReferenceExpression : public Expression
{
public:
wstring    name;
};
//非捕獲組
class NoneCaptureExpression : public Expression
{
public:
Ptr<Expression> expression;
};
//命名後向引用
class BackReferenceExpression : public Expression
{
public:
wstring name;
};
class AnonymityBackReferenceExpression : public Expression
{
public:
int index;
};

class NegativeLookbehindExpression : public Expression
{
public:
Ptr<Expression> expression;
};
class PositiveLookbehindExpression : public Expression
{
public:
Ptr<Expression> expression;
};
class NegativeLookaheadExpression : public Expression
{
public:
Ptr<Expression> expression;
};
class PositivetiveLookaheadExpression : public Expression
{
public:
Ptr<Expression> expression;
};

 

除了多了幾個型別和V大的語法樹型別差不多,然後是語法樹遍歷和構造部分,這塊參考v大的博文就好.
http://www.cppblog.com/vczh/archive/2009/10/18/98862.html 語法樹的構造與遍歷
http://www.cppblog.com/vczh/archive/2009/10/18/98873.html 字符集和正規化
NFA構造,DFA構造:
這塊歡迎去看v大的擴充套件正規表示式構造方法的博文.http://www.cppblog.com/vczh/archive/2008/05/22/50763.html
不過我的寫法不太一樣.v大整個正規表示式,所有節點都在一張圖上.因為有命令邊和end邊來控制子表示式範圍.我沒有加入end邊.所以我是用子圖的方式.命令邊上繫結一個子表示式是的索引.
匹配到命令邊後,根據索引去找子表示式去匹配.
關於有向圖這塊,用shared_ptr為了避免迴圈引用,推薦弄個shared_ptr的節點陣列.節點池.暴露出原始指標來操作.只要記得自己別蛋疼delete就木有事了.
在ENFA到NFA的轉換這塊.V大直接把大部分邊都合併了....合併所有不消耗字元的邊是很酷炫...不過我自己的邊的資料結構(vector<Edge*>)遇到這種會有些問題...當不消耗字元的邊匹配失敗後,因為所有後續邊都拿過來了(不知道我在什麼的可以LSURL先看v大那兩篇文章).
會不知道重啟匹配的下一條邊位置.所以我沒這麼合併...只是合併了空邊.為了保證每個子表示式都有唯一的開始和結束節點,我加入了Final邊連結子表示式末尾,在Final上無條件匹配成功,但是final邊不會被優化掉.
這裡有個難點就是逆向LookAround的處理.因為是逆向的.所以裡面的子表示式要返過來匹配.例如34(<=34)54.匹配3454當正規表示式到達4和5之間的位置.啟動逆向環視.匹配字元的順序是先匹配4再匹配3.而不是子表示式裡面寫的順序(<=34).
所以這裡的問題就是在構造完全部NFA後,遍歷NFA一遍,把NFA的第一層(每個子表示式都被綁在功能邊上)的逆向LookAround邊下屬的表示式子圖全部逆圖.並且如果子圖中包含了LookAround,要反向(例如蛋疼的(?<=3(?=5))這樣的寫法- -....這樣才能保證匹配成功.
匹配的演算法難點:
匹配演算法...難點有一個,需要輸入串用迭代器指向.而不是普通的索引來遍歷字串.因為在逆向環視中,需要逆向匹配方向.普通的索引int型別沒法反向,所以用迭代器比較方便,套個反向迭代器就OK了- -.因為子圖會呼叫匹配演算法去匹配.所以
這裡編譯時是個遞迴的過程.so...反向迭代器的巢狀(不斷地進入LookBehind邊時候套層新的反向迭代器)會導致模板編譯遞迴.這裡需要分解LookBehind邊的處理函式.對於由反向迭代器傳入的索引.呼叫.base()對於正向迭代器傳入的,呼叫reverse_iter(iter)構造反向.
大概設計是:當前狀態的子圖有DFA就用DFA匹配,沒有DFA就用NFA匹配.NFA測試每一條邊,NFA匹配過程中的每一個狀態都要壓棧.匹配成功進入下一狀態,失敗就嘗試下一條邊,如果當前狀態所有邊都失敗,pop當前狀態.新的back()的狀態恢復.執行下一條邊.如此往復.
編譯選項的設計:
ExplicitCapture,//不使用匿名捕獲組功能,詞法分析的時候做,匹配到"("時候,如果optional裡面包含了這個,就返回NoneCatupure Token
IgnoreCase,//大小寫不敏感的匹配,構造字符集和正交化的時候處理.遍歷字符集的時候加上大寫或者小寫部分.
Multiline,// $^ 匹配行結尾和開頭 轉換為LookAround
RightToLeft,// 使用逆向迭代器傳入input串
Singleline,//更改"."代表的字符集合範圍.
難點大概就這些吧...其他的看v大的博文就知道了,寫的都很棒.好頂贊.
整個專案大概5000行程式碼,從各個部落格和,NET正則引擎參考 MSDN上找了幾百個不同測試樣例,寫完真心感覺學到了不少東西- -....雖然C++已經寫了快2年了,很多低階錯誤還是會犯...,emplace_back和push_back的區別.還有& *的混合使用的錯誤(對指標的引用)...搭配上有向圖這種除錯起來麻煩的東東- -...推薦寫個列印DFA和NFA有向圖資訊的debug輔助函式,比斷點除錯快多了
希望以後造輪子可以少犯點低階錯誤..
再次感謝V大的博文指導!^_^
差不多今年第一次寫部落格...希望以後多寫寫:)
參考資料:
1.http://www.cppblog.com/vczh/category/12070.html?Show=All
2.http://www.cppblog.com/vczh/archive/2008/05/22/50763.html
3.編譯原理的龍書

相關文章