原创 正则引擎完工,记录下思路和设计
2014-10-26 00:11 IceCrystals 阅读(1255) 评论(3) 编辑 收藏 举报最近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.编译原理的龙书