開發起始,花了必定的時間調研尋找一個好的語法解析器,由於在表格安全性檢查過程當中須要解析各類形式靈活的檢查規則,因此須要一個相似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這個語言,發揮其強大的解析功能用於導表的安全性檢查中,固然這就是另一個話題了。