开发起始,花了必定的时间调研寻找一个好的语法解析器,由于在表格安全性检查过程当中须要解析各类形式灵活的检查规则,因此须要一个相似lex/yacc这样具备强大语言解析功能,但语法规则又能够轻量级配置的解析器,最后选择了一种近似上下文无关CFG(context-free-grammar)的语言PEG(parsing-expression-grammar),做为咱们编写检查规则的基础文法。 python
S <-- aSb 正则表达式
这两条规则产生的语言S 就等于{a nb n, n >= 1}。而语言{a nb n c n, n >= 1}虽然也具有有限的肯定元素集合,但不能由递归级联合并规则所产生,因此其不属于上下文无关语言。 算法
然而上下文无关文法(CFG)却由于其规则的灵活性和语法解析的多样性,在工程运用上存在着障碍。CFG文法属于一种能够自顶向下解析的语言,若是一个字符串属于一种CFG语言,那么能够将这个字符串自顶向下逆向推导出构建的过程,产生一个语法解析树,而对于大多数CFG语言来讲,其合法字符串能够被解析成多个不一样结构的语法树,譬如以下的CFG语言(例子2)S <-- S+S express
解析字符串a+a+a就会获得两种不一样的语法解析树,以下所示: 缓存
S S3
/ \
/ \
S1 ZeroOrMore(+ (S2 | S1)
| / \
| / \ 安全
a + S2
|
| 数据结构
S3
/ \
/ \
S1 ZeroOrMore(+ (S2 | S1)
| / \
| / \
a + S1
|
|
a 架构
因为PEG语言自己对级联规则的限制性,消除了语法解析过程当中的歧义性,并且理论上能够保证绝大多数CFG语言,只要稍加修改相应的语法规则均可以转换成相应的PEG语言(对于存在左递归的CFG语言没法用PEG语法来解析),而且能够保证解析过程的惟一性,这样就为工程的应用提供了便利性。 less
如今已经有不少用不一样程序语言开发的开源库来支持解析peg语言,因为项目自己的缘由,咱们选择使用了python实现的开源库pyparsing(1.5.6)。pyparsing的架构主要有两大部分组成,分别为用来表示解析结果的ParseResults和表示解析规则和解析算法的ParserElement,前者表示语法解析过程当中所操做的数据结构,后者表示具体的语法解析流程。 ide
1. 数据结构
ParseResults是算法所操做的基本数据结构,用来表示解析字符串后所获得语法解析树。单个Parseresults的数据结构用来表示一棵树的某个节点,内部还能够包含相应子节点ParseResults结构。一个节点ParseResults的子节点序列用一个list来表示,能够经过访问序号来获取;同时为了快速的定位子树中一些有用的子节点,还能够将ParseResults按照dict来操做,经过字符串key快速定位。对于以某个ParseResults节点(PS)为根的解析树,全部该层子节点能够经过链表的形式获得,如PS[0],PS[1],…;对于在语法表达式中用setResultsName(‘xxx’)定义的token,均可以用字典以key为索引的方式获得,如PS[‘xxx’]
ParseResults结构以下所示:
class ParseResults: member: __toklist member: __tokdict member: __accumNames member: __parent
其中__toklist主要维护了子节点的链表结构,__tokdict维护了字典结构,__accumNames保存了__tokdict中非modal形式(多个相同key的value只取当前最后一个)的key值集合,__parent指向了父节点。其中__toklist中的元素能够是string或者做为子节点ParseResults结构;__tokdict的key为string,value是ParseResultsWithOffset的数据结构,ParseResultsWithOffset是一个包含有两个元素的pair tuple,tuple[0]为__toklist中的元素,tuple[1]为该元素在__toklist中的index,这样该元素便可以经过PS[index]来提取,也能够经过PS[key]来提取。下图表示在一个ParseResults中在index为2的位置插入key为’YYY’的节点E:
ParseResults主要经过重载python中的__getitem__,__setitem_,__delitem__,__iadd__这三个方法来统一实现了list和dict相应操做。
# 重载[]操做 def __getitem__( self, i ): if isinstance( i, (int,slice) ): # 若是i是整数或者slice,则看成链表操做 return self._toklist[i] else: # 若是传入的i是str类型的key,则看成字典操做 if i not in self._accumNames: # 当parseresult在建立时,参数modal为True时,则返回values中的最后一个 return self._tokdict[i][-1][0] else: #非modal形式,则将全部values按照一个list返回 return ParseResults([ v[0] for v in self._tokdict[i] ])
# 重载[]操做 def __setitem__( self, k, v, isinstance=isinstance ): if isinstance(v,_ParseResultsWithOffset): #字典操做非modal模式下,放在全部values的末尾 self._tokdict[k] = self._tokdict.get(k,list()) + [v] sub = v[0] elif isinstance(k,int): # 若是key为int就代表链表操做,直接取代原来的value self._toklist[k] = v sub = v else: # 组装ParseResultsWithOffset做为value self._tokdict[k] = self._tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)] sub = v if isinstance(sub,ParseResults): # 申明父节点 sub._parent = wkref(self)
# 重载+=操做,其中 + 操做也是由+=来实现 def __iadd__( self, other ): if other._tokdict: # 合并dict的操做 offset = len(self._toklist) addoffset = ( lambda a: (a<0 and offset) or (a+offset) ) otheritems = other._tokdict.items() otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) ) for (k,vlist) in otheritems for v in vlist] for k,v in otherdictitems: self[k] = v if isinstance(v[0],ParseResults): v[0]._parent = wkref(self) # 合并list的操做 self._toklist += other._toklist # 更新key的集合 self._accumNames.update( other._accumNames ) return self
def __delitem__( self, i )在当前版本中的实现是错误的,当以dict的方式进行删除时,即传入的i不为int的状况,只简单地删除dict中的值,而没有更新列表中剩余的elements的序号,即index值,好在peg的解析过程并不涉及dict删除状况。
为了更好的阐述ParseResults数据结构,对上面的语法(例子3)稍做改变,添加了对括号对称检查的语法解析(例子4):
S1 <-- Literal('a')
S2 <-- Literal('(') + S3 + Literal(')')
S3 <-- (S2 | S1 ) + ZeroOrMore(Group(Literal('+') + (S2 | S1)).setResultsName(‘right token'))
则字符串(a + a) + a解析的结果为
__tokdict : {'right token': [((['+', 'a'], {}), 2), ((['+', 'a'], {}), 4)]}
__toklist : ['(', 'a', ['+', 'a'], ')', ['+', 'a']]
图示以下:
2. 算法流程:
ParserElement是解析peg语法的基本组成元素,由有限肯定集合(token)和相应的限制级联规则如 Zero-or-more,One-or-more,Forward等所组成,能够将这类级联规则当作例子4里面的语法规则。ParserElement的大体架构图以下:
图中Token表示有限肯定集合和简单的级联规则如Sequence等, ParseExpression表示单元或者双元级联规则如And和Or,ParseElementEnhance表示具备递归性的级联规则如Forward,正是因为ParseElementEnhance这类语法的引入,才加强了PEG的解析能力,将其与正则表达式的解析区分开来。
ParserElement的每一个继承类型都实现了相应的匹配规则,匹配规则的实现都是由函数parseImpl来定义的:
def parseImpl( self, instring, loc, doActions=True )
其中instring表示须要匹配的字符串,loc表示开始匹配的字符串位置,doActions表示在匹配成功以后执行操做函数,通常有用户提供,而且这些操做的定义将直接影响到后续语法解析过程可否进行优化,这将在后面说明。函数将返回匹配终止时指针所在的index和表示相应匹配结果的ParseResults。
Token中的各个匹配规则相对比较简单,包括简单的字符串匹配(Literal,Keyword),正则表达式匹配(Reg)等,以Keyword为例,算法只要求两个字符串相同:
def parseImpl( self, instring, loc, doActions=True ): if self.caseless: # 若是忽略大小写 if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and (loc == 0 or instring[loc-1].upper() not in self.identChars) ): return loc+self.matchLen, self.match else: if (instring[loc] == self.firstMatchChar and # 真正的匹配过程,第一个字符相等(优化),字符串匹配startswith (self.matchLen==1 or instring.startswith(self.match,loc)) and (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and (loc == 0 or instring[loc-1] not in self.identChars) ): return loc+self.matchLen, self.match #~ raise ParseException( instring, loc, self.errmsg ) # 匹配失败的状况 exc = self.myException exc.loc = loc exc.pstr = instring raise exc
加强型级联规则的引入特别是Forward等递归类型的引入,加强了peg语言的解析能力,咱们都知道CFG的语言表达性要强于正则表达式,是由于CFG中引入了一种递归级联的能力,而Forward正是为PEG注入了这种递归能力,咱们就以Forward为例来看一下递归匹配规则:
def parseImpl( self, instring, loc, doActions=True ): if self.expr is not None: # 递归调用包含规则 return self.expr._parse( instring, loc, doActions, callPreParse=False ) else: raise ParseException("",loc,self.errmsg,self)
Forward的parseImpl继承于ParseElementEnhance,只是递归地调用了子表达式expr的匹配规则,但其真正的亮点是它重载了python中的<<操做符
def __lshift__( self, other ): if isinstance( other, basestring ): other = Literal(other) # 经过这个赋值,实现了递归语法定义,能够在other中包含本身的表达式,但不能出现左递归,不然程序将陷入死循环,这也是peg语言惟一的限制条件 self.expr = other … return None
分析了ParserElement的组成元素和相应的匹配规则,下面来说讲PEG的解析算法。PEG的解析算法是一个简单递归深度优先解析过程,递归穷举全部匹配过程,若是匹配就继续深刻解析,若是穷举完全部可能匹配规则后失败,则回溯深度尝试其余匹配可能。
匹配算法入口是:def parseString( self, instring, parseAll=False ): # 重置匹配cache,为parse rat全部 ParserElement.resetCache() if not self.streamlined: self.streamline() #~ self.saveAsList = True for e in self.ignoreExprs: e.streamline() if not self.keepTabs: instring = instring.expandtabs() try: # 递归调用各个子ParserElement相应的匹配规则 loc, tokens = self._parse( instring, 0 ) if parseAll: # 要整个字符串彻底匹配的状况 se = Empty() + StringEnd() se._parse( instring, loc ) except ParseBaseException: if ParserElement.verbose_stacktrace: raise else: # catch and re-raise exception from here, clears out pyparsing internal stack trace exc = sys.exc_info()[1] raise exc else: return tokens
其中_parse函数就是咱们刚才分析的各个组成元素的parseImpl函数。若是这个element是ParseElementEnhance,则会在相应的parseImpl函数中递归调用各个子规则的parseImpl函数。
因为深度优先解析算法经过向前(look ahead)和回溯(look backward)尝试全部匹配可能,因此在最差的状况下会呈指数级时间复杂度。工程上为了优化解析的效率,引入了packrat parser算法,其本质是缓存全部的中间解析结果,节省了重复解析的时间,在最好的状况下能够达到线性的时间复杂度。在pyparsing中能够经过设置标志位
_packratEnabled = True
来激活packrat parser算法,这样各个ParserElement的parseImpl匹配规则被封装,以下所示:
# this method gets repeatedly called during backtracking with the same arguments - # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression def _parseCache( self, instring, loc, doActions=True, callPreParse=True ): lookup = (self,instring,loc,callPreParse,doActions) if lookup in ParserElement._exprArgCache: # 尝试着从cache中找匹配结果 value = ParserElement._exprArgCache[ lookup ] if isinstance(value, Exception): raise value return (value[0],value[1].copy()) else: try: #在没有找到的状况下,才真正作解析流程 value = self._parseNoCache( instring, loc, doActions, callPreParse ) ParserElement._exprArgCache[ lookup ] = (value[0],value[1].copy()) return value except ParseBaseException: pe = sys.exc_info()[1] ParserElement._exprArgCache[ lookup ] = pe Raisepackrat parser算法虽然提升了解析算法的效率,但也对解析算法作了必定的限制,其要求在解析过程当中全部匹配成功后用户所定义的动做函数(经过 setParseAction来定义)不能修改原始的字符串,不然 cache将会出错。同时 packrat parser算法是一个空间换时间的算法,过程当中 cache将消耗不少内存,为此 pyparsing也作了必定的优化,对于每个 ParseResults都重载了 __new__函数 ,限制产生ParseResults实体的个数,代码以下:
def __new__(cls, toklist, name=None, asList=True, modal=True ): if isinstance(toklist, cls): # 空间优化,若是是解析的中间结果,将不产生新的parserresults,只是重用和修改原有的instance return toklist retobj = object.__new__(cls) retobj.__doinit = True return retobjpyparsing大概就分析到这里,其实以后还有不少的工做花在如何用好PEG这个语言,发挥其强大的解析功能用于导表的安全性检查中,固然这就是另一个话题了。