引言
最近剛剛用python寫完了一個解析protobuf檔案的簡單編譯器,深感ply實現詞法分析和語法分析的簡潔方便。乘著餘熱未過,頭腦清醒,記下一點總結和心得,方便各位pythoner參考使用。
ply使用
簡介
如果你不是從事編譯器或者解析器的開發工作,你可能從未聽說過ply。ply是基於python的lex和yacc,而它的作者就是大名鼎鼎Python Cookbook, 3rd Edition的作者。可能有些朋友就納悶了,我一個業務開發怎麼需要自己寫編譯器呢,各位程式設計大牛說過,中央決定了,要多嘗試新的東西。而且瞭解一些語法解析的姿勢,以後自己解析格式複雜的日誌或者數學公式,也是非常有幫助的。
針對沒有編譯基礎的童鞋,強烈建議瞭解一些文法相關的基本概念。輪子哥強烈推薦的parsing techniques以及編譯龍虎鯨書,個人感覺都不適合入門學習,在此推薦胡倫俊的編譯原理(電子工業出版社),針對概念的例子講解很多,很適合入門學習。當然也不需要特別深入研究,知道詞法分析和語法分析的相關概念和方法就可以愉快的使用ply了。文件連結: http://www.pchou.info/open-source/2014/01/18/52da47204d4cb.html
為了方便大家上手,以求解多元一次方程組為例,講解一下ply的使用。
例子說明
輸入是多個格式為x + 4y - 3.2z = 7
的一次方程,為了讓例子儘可能簡單,做如下限制:
- 每個方程含有變數的部分在等號左邊,常數在等號右邊
- 每個方程不限制變數的個數以及變數的順序,但每個方程每個變數只允許出現一次
- 變數的命令規則為小寫字母串(x y xx yy abc 均為合法變數名)
- 變數的係數限制為整數和浮點數,浮點數不允許
1.4e8
的格式,係數和變數緊鄰,且係數不能為0 - 方程組和方程組之間用
, ;
隔開
學過線性代數的童鞋肯定知道,只需要將方程組抽象為矩陣,按照線性代數的方法就可以解決。因此只需要將輸入方程組解析成右邊的矩陣和變數列表即可,剩下的求解過程就可以交給線性代數相關的工具解決。
詞法解析
ply中的lex來做詞法解析,詞法解析的理論有一大堆,但是lex用起來卻非常直觀,就是用正規表示式的方式將文字字串解析為一個一個的token,下面的程式碼就是用lex實現詞法解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
from ply import lex # 空格 製表符 回車這些不可見符號都忽略 t_ignore = ' \t\r' # 解析錯誤的時候直接丟擲異常 def t_error(t): raise Exception('error {} at line {}'.format(t.value[0], t.lineno)) # 記錄行號,方便出錯定位 def t_newline(t): r'\n+' t.lexer.lineno += len(t.value) # 支援c++風格的\\註釋 def t_ignore_COMMENT(t): r'\/\/[^\n]*' # 變數的命令規則 def t_VARIABLE(t): r'[a-z]+' return t # 常數命令規則 def t_CONSTANT(t): r'\d+(\.\d+)?' t.value = float(t.value) return t # 輸入中支援的符號頭token,當然也支援t_PLUS = r'\+'的方式將加號定義為token literals = '+-,;=' tokens = ('VARIABLE', 'CONSTANT') if __name__ == '__main__': data = ''' -x + 2.4y + z = 0; //this is a comment 9y - z + 7.2x = -1; y - z + x = 8 ''' lexer = lex.lex() lexer.input(data) while True: tok = lexer.token() if not tok: break print tok |
直接執行檔案就可以將解析的token串列印出來,如下所示,詳細的使用文件可以參考ply文件。
1 2 3 4 5 6 7 8 9 10 |
LexToken(-,'-',2,5) LexToken(VARIABLE,'x',2,6) LexToken(+,'+',2,8) LexToken(CONSTANT,2.4,2,10) LexToken(VARIABLE,'y',2,13) LexToken(+,'+',2,15) LexToken(VARIABLE,'z',2,17) LexToken(=,'=',2,19) LexToken(CONSTANT,0.0,2,21) LexToken(;,';',2,22) |
語法解析
ply中的yacc用作語法分析,雖然複雜的詞法分析可以代替簡單的語法分析,但類似於程式語言的解析再複雜的詞法分析也勝任不了。在使用yacc之前,需要了解上下文無關文法,這部分內容太多太雜,我也只瞭解部分簡單的概念,有興趣的可以看一看編譯原理深入瞭解。
目前語法分析的方法有兩大類,即自下向上的分析方法和自上而下的分析方法。所謂自上而下的分下法就是從文法的開始符號出發,根據文法規則正向推到出給定句子的一種方法,或者說,從樹根開始,往下構造語法樹,直到建立每個樹葉的分析方法。代表演算法是LL(1),此演算法文法解析能力不強,對文法定義要求比較高,主流的編譯器都沒有使用。自下而上的分析法是從給定的輸入串開始,根據文法規則逐步進行歸約,直至歸約到文法的開始符號,或者說從語法書的末端開始,步步向上歸約,直至歸約到根節點的分析方法。代表演算法有SLR、LRLR,ply使用的就是LRLR。
因此我們只需要定義文法和規約動作即可,以下就是完整的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# -*- coding=utf8 -*- from ply import ( lex, yacc ) # 空格 製表符 回車這些不可見符號都忽略 t_ignore = ' \t\r' # 解析錯誤的時候直接丟擲異常 def t_error(t): raise Exception('error {} at line {}'.format(t.value[0], t.lineno)) # 記錄行號,方便出錯定位 def t_newline(t): r'\n+' t.lexer.lineno += len(t.value) # 支援c++風格的\\註釋 def t_ignore_COMMENT(t): r'\/\/[^\n]*' # 變數的命令規則 def t_VARIABLE(t): r'[a-z]+' return t # 常數命令規則 def t_CONSTANT(t): r'\d+(\.\d+)?' t.value = float(t.value) return t # 輸入中支援的符號頭token,當然也支援t_PLUS = r'\+'的方式將加號定義為token literals = '+-,;=' tokens = ('VARIABLE', 'CONSTANT') # 頂層文法,規約的時候equations對應的p[1]是一個列表,包含了方程左邊各個變數與係數還有方程左邊的常數 def p_start(p): """start : equations""" var_count, var_list = 0, [] for left, _ in p[1]: for con, var_name in left: if var_name in var_list: continue var_list.append(var_name) var_count += 1 matrix = [[0] * (var_count + 1) for _ in xrange(len(p[1]))] for counter, eq in enumerate(p[1]): left, right = eq for con, var_name in left: matrix[counter][var_list.index(var_name)] = con matrix[counter][-1] = -right var_list.append(1) p[0] = matrix, var_list # 方程組對應的文法,每個方程用,或者;做分隔 def p_equations(p): """equations : equation ',' equations | equation ';' equations | equation""" if len(p) == 2: p[0] = [p[1]] else: p[0] = [p[1]] + p[3] # 單個方程對應的文法 def p_equation(p): """equation : eq_left '=' eq_right""" p[0] = (p[1], p[3]) # 方程等式左邊對應的文法 def p_eq_left(p): """eq_left : var_unit eq_left |""" if len(p) == 1: p[0] = [] else: p[0] = [p[1]] + p[2] # 六種文法對應例子: x, 5x, +x, -x, +4x, -4y # 歸約的形式是一個元組,例: (5, 'x') def p_var_unit(p): """var_unit : VARIABLE | CONSTANT VARIABLE | '+' VARIABLE | '-' VARIABLE | '+' CONSTANT VARIABLE | '-' CONSTANT VARIABLE""" len_p = len(p) if len_p == 2: p[0] = (1.0, p[1]) elif len_p == 3: if p[1] == '+': p[0] = (1.0, p[2]) elif p[1] == '-': p[0] = (-1.0, p[2]) else: p[0] = (p[1], p[2]) else: if p[1] == '+': p[0] = (p[2], p[3]) else: p[0] = (-p[2], p[3]) # 方程等式右邊對應的常數,對應的例子:1.2, +1.2, -1.2 def p_eq_right(p): """eq_right : CONSTANT | '+' CONSTANT | '-' CONSTANT""" if len(p) == 3: if p[1] == '-': p[0] = -p[2] else: p[0] = p[2] else: p[0] = p[1] if __name__ == '__main__': data = ''' -x + 2.4y + z = 0; //this is a comment 9y - z + 7.2x = -1; y - z + x = 8 ''' lexer = lex.lex() parser = yacc.yacc(debug=True) lexer.lineno = 1 s = parser.parse(data) print s |
直接執行檔案即可,得到的輸出如下,之後就可以根據線性代數的方法求解各個變數的值
1 |
([[-1.0, 2.4, 1.0, -0.0], [7.2, 9.0, -1.0, 1.0], [1.0, 1.0, -1.0, -8.0]], ['x', 'y', 'z', 1]) |
總結
依託於python簡潔的語法,ply為我們提供了一個強大的語法分析工具,更復雜的例子可以參考https://github.com/LiuRoy/proto_parser,這是我用ply實現的一個簡單的protobuf解析器,用於減少頻繁的中間檔案生成。有這種神器,一顆賽艇!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!