Ply 是一個純 python 的詞法分析和語法分析庫,包括兩個模塊:lex 和 yacc
Ply 是一個純 python 的詞法分析和語法分析庫,包括兩個模塊:lex 和 yacc
使用 lex 詞法分析最重要的是定義 token 及其解析規則,每個詞法分析程序都必須定義 tokens
元組用于聲明 TOKEN:
tokens = ( 'NUMBER', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN',)
TOKEN 的解析規則通過 t_token_name
來定義,支持這樣三種方式:
對于非常簡單的規則,你可以簡單地定義上面格式的字符串去聲明,如: t_PLUS = r'+'
對于復雜的規則,你可以定義如下格式簽名的函數去聲明:
def t_NUMBER(t: lex.LexToken): r'd+' t.value = int(t.value) return t
正則表達式在函數的文檔字符串中指定, 參數固定是 lex.LexToken
的實例,它包含四個基本屬性:
type
: 類型,就是 tokens 中定義的某個字符串value
: 對應的值lineno
: 第幾行lexpos
: 文本起始位置偏移值如果你的表達式更加復雜,由多個子表達式組合而成,文檔字符串無法滿足時就可以使用 @TOKEN
注解,如:
digit = r'([0-9])' nondigit = r'([_A-Za-z])' identifier = r'(' + nondigit + r'(' + digit + r'|' + nondigit + r')*)' @TOKEN(identifier) def t_ID(t): # want docstring to be identifier above. ????? return t
需要注意的是 tokens 列表中的 TOKEN 是有順序的,靠前的 TOKEN 將優先被解析,如在定義 =
和 ==
的時候,你可能就需要將后者放在前面。
跳過注釋:
def t_COMMENT(t): r'#.*' pass # No return value. Token discarded
或者,您可以在token聲明中包含前綴“ignore_”,以強制忽略token。例如: ` t_ignore_COMMENT = r'#.*'
2. 定義行:您可以使用
t_newline(t)告訴詞法分析器什么是一個新行,這樣分析器就可以正確地更新
lineno` 了,如:
def t_newline(t): r'n+' t.lexer.lineno += len(t.value)
定義錯誤信息,當詞法分析出現錯誤時,你應該明確的告訴用戶哪兒錯了,使用 t_error
來聲明錯誤提示信息,如下:
def t_error(t): print(f"Illegal character '{t.value}' in {t.lineno}:{t.lexpos}") t.lexer.skip(1)
EOF 處理:有時你需要告訴解析器什么時候該結束,又或者你不想一次性將要解析的源文件加載到內存中,想逐批加載分析,這時候可以使用 t_eof(t)
告訴解析器結束時該干什么:
def t_eof(t): # Get more input (Example) more = raw_input('... ') if more: self.lexer.input(more) return self.lexer.token() return None
單個字符可以使用 literals
更方便地指定,如:
literals = [ '+','-','*','/' ]# 或literals = "+-*/"
但需要注意的是這樣指定得到的 value 和 type 都是字符本身,你可以像下面這樣編寫代碼修改這個行為:
def t_add(t: lex.LexToken): r'+' t.type = "ADD" return t
有時你可能想定義一些關鍵字,如 if else
之類的,為每個關鍵字定義解析規則可能有點麻煩,這時候將他們作為單詞的一部分去解析可能更高效:
tokens = ("char") reserved2 = { 'if' : 'IF', 'then' : 'THEN', 'else' : 'ELSE', 'while' : 'WHILE', } tokens += tuple(reserved2.values()) def t_CHAR(t): r'[a-zA-Z_][a-zA-Z_0-9]*' t.type = reserved2.get(t.value,'ID') return t
跳過空格之類無意義的填充符號有時也是非常必要的,你可以使用 t_ignore
標注這些字符,可以放心的是當這些字符被包含在其他規則中時,它將不會被忽略,使用如下:
t_ignore = (" ")
通過上面的介紹,你可能已經發現,ply 包含太多特殊規則了,對于一個不了解 ply 的人來說,這可能太糟糕了,我們需要一些辦法來稍稍改善它。
你可以在單獨的模塊中定義規則,以此保證分析器主代碼干凈,這需要你在創建 lexer 時顯式地指定 module:
lexer = lex.lex(module=tokrules)
面向對象:有時面向對象不失是一個封裝的好辦法,你可以以面向對象的方式編寫規則,如下:
import ply.lex as lex class MyLexer: reserved = { 'if': 'IF', 'then': 'THEN', 'else': 'ELSE', 'while': 'WHILE', } tokens = ("NUMBER", "char") + tuple(reserved.values()) literals = ['+', '-', '*', '/'] t_ignore = (" ") def __init__(self, **kwargs) -> None: self.lexer: lex.Lexer = lex.lex(module=self, **kwargs) def t_NUMBER(self, t: lex.LexToken) -> lex.LexToken: r'd+' t.value = int(t.value) return t def t_CHAR(self, t: lex.LexToken) -> lex.LexToken: r'[a-zA-Z_][a-zA-Z_0-9]*' t.type = self.reserved.get(t.value, 'char') return t def t_error(self, t: lex.LexToken) -> lex.LexToken: print(f"Illegal character '{t.value}' in {t.lineno}:{t.lexpos}") t.lexer.skip(1) def run(self, data): self.lexer.input(data) while True: tok = self.lexer.token() if not tok: break print(tok) if __name__ == "__main__": data = "if +12 +3" lexer = MyLexer() lexer.run(data)
當然,你也可以用閉包去做,但我個人是一個徹底的閉包反對者,所以不多做介紹……
考慮你正在寫一個 MarkDown 的分析器,你可能需要做這樣的事情:
要處理這樣的需求最好是給分析器提供不同的狀態和指定在某種狀態下的解析規則,在 ply 中,你可以使用 states
定義一組狀態:
states = ( ('py','exclusive'), ('c','inclusive'), )
每種狀態有兩種類別,分別是 exclusive 和 inclusive:exclusive 表示獨占,編譯器跳轉到這種狀態時將會完全使用該狀態的詞法規則覆蓋原來的規則,例如上面的例子就適合 exclusive 類型;inclusive:exclusive 表示包含,跳轉到這種狀態時,編譯器將會將該狀態的規則追加到原來的規則列表中。
一旦定義了 states 你就需要在定義每個規則時顯式聲明它屬于那種狀態,方法如下:
t_py_NUMBER = r"d+"def t_c_error(t: lex.LexToken): pass
如果你的規則適用于任何狀態,可以使用 ANY
簽名:
def t_ANY_newline(t): r'n' t.lexer.lineno += 1
當然,顯式地指定每個狀態也是可以的,像這樣:
def t_py_c_newline(t): r'n' t.lexer.lineno += 1
lexing 的默認狀態叫 INITIAL
, 你可以在規則中通過 begin
函數切換狀態,如:
def t_md_CHAR(t): r'[a-zA-Z_][a-zA-Z_0-9]*' if t.value == "```py": t.lexer.begin("py") elif t.value == "```": t.lexer.begin("INITIAL") return t
使用 push_state()
和 pop_state()
也可以完成相同的操作,如:
def t_md_CHAR(t): r'[a-zA-Z_][a-zA-Z_0-9]*' if t.value == "```py": t.lexer.push_state("py") elif t.value == "```": t.lexer.pop_state() return t
ply 使用 LR 解析,關鍵模塊是 ply.yacc
, 類似于詞法分析,你需要按照一定的格式定義你的語法分析規則,假設給定以下語法規范:
expression : expression + term | expression - term | term term : term * factor | term / factor | factor factor : NUMBER | ( expression )
它是一個簡單地算數表達式的語法規則,在 ply 中,我們可以這樣去描述它:
def p_expression_plus(p): '''expression : expression '+' term''' p[0] = p[1] + [3]def p_expression_minux(p): '''expression : expression '-' term''' p[0] = p[1] - [3]def p_expression_term(p): '''expression : term''' p[0] = p[1]def p_term_times(p): '''term : term "*" factor''' p[0] = p[1] * [3]def p_term_p(p): '''term : term "/" factor''' p[0] = p[1] / [3]def p_term_factor(p): '''term : factor''' p[0] = p[1]def p_factor_num(p): '''factor : NUMBER''' p[0] = p[1]def p_factor_expr(p): '''factor : "(" expression ")"''' p[0] = p[2]
作為一種更簡潔的寫法,你可以將多個語法規則寫到一個描述函數中,就像下面這樣:
def p_expression(p): '''expression : expression "+" term | expression "-" term | term''' match p[2]: case "+" if len(p) > 3: p[0] = p[1] + p[3] case "-" if len(p) > 3: p[0] = p[1] - p[3] case _ if len(p) == 2: p[0] = p[1]
你當然可以可以將所有規則放在一個函數中,但出于可讀性和性能,請不要那樣做。
你可能注意到了上面示例中的單個字符如 +-*/
都被引號印了起來,這是有必要的,這種做法對應詞法分析中講過的 literals
如果你不喜歡使用它,可以使用更普遍的做法:
def p_expression_plus(p): '''expression : expression PLUS term''' p[0] = p[1] + [3]
你可以使用特殊的 p_empty
定義空結果的處理方案,你也可以在任何規則中使用 empty
表示一個空結果,就像下面這樣:
def p_empty(p): 'empty :' pass def p_optitem(p): 'optitem : item' ' | empty' ...
還需要注意的是你定義的第一條規則將被默認作為頂級語法規則,你可以使用 start
對其進行修改,如:
def p_foo(p): '''bar : A B'''start = "foo"# or parser = yacc.yacc(start="foo")
上面給出的語法規則是經過規約的規則,對解析器來說,它更容易處理,因為它幾乎不存在歧義,但從編程的角度來說,我們可能會以一種更符合人類直覺的方式定義語法規則,就像下面這樣:
expression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression | LPAREN expression RPAREN | NUMBER
它如此簡單,但存在大問題,考慮一個輸入: 3+4*5
: 語法分析步驟如下:
Step SymbolStack Input Tokens Action-----------------------------------------------------------------------------1 $ 3*4+5$ Shift 32 $3 *4+5$ Redius: expr : NUMBER3 $expr *4+5$ Shift *4 $expr * 4+5$ Shift 45 $expr * 4 +5$ Redius: expr : NUMBER6 $expr * expr +5$ !Shift + or Redius expr : expr * expr
當分析進行到第六步時,分析器不能確定應該是彈出 PLUS 還是對表達式 expr * expr
應用規則: expr : expr * expr
,默認情況下,解析器會選擇移入,即彈入 PLUS,這顯然錯了,因此我們需要指定規則的優先級:
precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'),)
precedence
中,TOKEN 優先級從小到大排列,上面的表達式聲明了加減的優先級小于乘除,且它們都是左關聯的。這些定義將被應用于每條語法規則,LR 語法中,語法規則的優先級總是由其最右面的富豪的優先級決定的。在進行語法分析時,將會按以下具體規則通過優先級解決沖突問題:
expr * expr
優先級由 *
決定就是 2,當前 TOKEN 如果是 +
, 優先級較小,就會對 expr * expr
進行規約expr + expr
優先級 1,當前 TOKEN 是 *
, 那就會移入 *
得到 expr + expr *
這里的一個漏洞是操作符在不同的上下文中可能有不同的優先級,考慮 3 - 4 * -2
其中的 -
在前面的用法中的優先級顯然低于后面一個用法的優先級,為了解決這個問題,可以設置虛擬 TOKEN:
precedence = ( ('left', 'PLUS', 'MINUS'), ('left', 'TIMES', 'DIVIDE'), ('right', 'UMINUS'), # Unary minus operator)def p_expr_uminus(p): 'expression : MINUS expression %prec UMINUS' p[0] = -p[2]
上面的例子中,設置了 UMINUS 的優先級最高,并在規則解析是,使用 %prec UMINUS
顯式指定了規則使用的優先級是 UMINUS
還有一種沖突被稱為 “規約/規約” 沖突,考慮以下語法規則:
assigment : char EQUALS NUMBER | char EQUALS expressionexpression : expression PLUS expression | expression MINUS expression | expression TIMES expression | expression DIVIDE expression | LPAREN expression RPAREN | NUMBER
當解析 a=5
時,我們應該應用 assigment : char EQUALS NUMBER
還是 assigment : char EQUALS expression
呢?當出現這種沖突時,yacc 會打印一下警告信息:
WARNING: 1 reduce/reduce conflictWARNING: reduce/reduce conflict in state 15 resolved using rule (assignment -> ID EQUALS NUMBER)WARNING: rejected rule (expression -> NUMBER)
上面的信息會告訴你發生了什么沖突,但并不會告訴你沖突是如何發生的,要了解語法分析的詳細流程,你肯呢個需要閱讀 parser.out
文件,該文件在語法分析器第一次運行時被生成,描述了語法分析的詳細流程,文件內容其實很容易理解,你需要注意下面三點:
state
相當于語法分析的一個分支,里面描述了在這個狀態下分析器允許輸入的 TOKEN 或表達式,其中的 .
表示當前位置。!
標注出了沖突的地方,雖然這些沖突不見得都是不好的。一個良好的解析器不應該遇到錯誤就立刻返回,你應該盡可能返回所有的錯誤以便用戶排查錯誤,你可以定義 p_error
來處理異常,它將以發生錯誤的 TOKEN 作為參數,在這里你可以做一些恢復錯誤的操作。
為了更好的追蹤問題,打印錯誤位置是十分必要的,你可以在構建 parser 時指定 tracking=True
來追蹤所有 TOKEN 的位置,當然,你也可以只追蹤特定表達式特定 TOKEN 的位置:
def p_expression(p): 'expression : expression "+" expression' line = p.lineno(2) # 追蹤 + 的位置 index = p.lexpos(2)
關于更詳細的 ply 的用法參見官方文檔,推薦一篇文章
最后附上上面例子中一個簡單計算器的完整程序:
import ply.lex as leximport ply.yacc as yaccclass MyLexer: # reserved = { # 'if': 'IF', # 'then': 'THEN', # 'else': 'ELSE', # 'while': 'WHILE', # } # tokens = ("NUMBER", "char") + tuple(reserved.values()) tokens = ("NUMBER", "char") literals = ['+', '-', '*', '/', '(', ')', '='] t_ignore = (" ") def __init__(self, **kwargs) -> None: self.lexer: lex.Lexer = lex.lex(module=self, **kwargs) def t_NUMBER(self, t: lex.LexToken) -> lex.LexToken: r'd+' t.value = int(t.value) return t def t_CHAR(self, t: lex.LexToken) -> lex.LexToken: r'[a-zA-Z_][a-zA-Z_0-9]*' # t.type = self.reserved.get(t.value, 'char') return t def t_error(self, t: lex.LexToken) -> lex.LexToken: print(f"Illegal character '{t.value}' in {t.lineno}:{t.lexpos}") t.lexer.skip(1) precedence = ( ('left', '+', '-'), ('left', '*', '/'), ('right', 'UMINUS'), ) def p_expression_uminus(self, p): '''expression : "-" expression %prec UMINUS''' p[0] = -p[2] def p_assignment(self, p): '''assignment : char "=" expression''' p[0] = p[3] def p_expression(self, p): '''expression : expression "+" expression | expression "-" expression | expression "*" expression | expression "/" expression | "(" expression ")" | NUMBER''' if len(p) == 4: match p[2]: case "+": p[0] = p[1] + p[3] case "-": p[0] = p[1] - p[3] case "*": p[0] = p[1] * p[3] case "/": p[0] = p[1] / p[3] elif len(p) == 3: p[0] = [2] elif len(p) == 2: p[0] = p[1] def run(self, data): self.lexer.input(data) self.parser = yacc.yacc(module=self, start="assignment") result = self.parser.parse(data) print(result)if __name__ == "__main__": data = "a=12+3 * 9 - 5*-3" lexer = MyLexer() lexer.run(data)
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
網絡推廣與網站優化公司(網絡優化與推廣專家)作為數字營銷領域的核心服務提供方,其價值在于通過技術手段與策略規劃幫助企業提升線上曝光度、用戶轉化率及品牌影響力。這...
在當今數字化時代,公司網站已成為企業展示形象、傳遞信息和開展業務的重要平臺。然而,對于許多公司來說,網站建設的價格是一個關鍵考量因素。本文將圍繞“公司網站建設價...
在當今的數字化時代,企業網站已成為企業展示形象、吸引客戶和開展業務的重要平臺。然而,對于許多中小企業來說,高昂的網站建設費用可能會成為其發展的瓶頸。幸運的是,隨...
霸王洗發膏怎么用?其實我也是最近才好好洗頭的??梢钥纯聪旅娴模赫_的洗發步驟:你總是覺得洗頭更容易嗎?每次洗完澡,順便把頭發弄濕,把洗發水抹在頭上,然后用水沖干凈。我相信這是大多數人的做法。但是,我必須很遺憾也很嚴肅地告訴你,你的洗頭工藝真的不對。正確的步驟應該是這樣的:洗頭前先梳頭。這樣可以讓你頭皮上的污垢和鱗片(也就是死細胞)松動,以便于下一步的清潔。第二步:3360打濕頭發,直到底層頭發和頂...
沈陽京哈高速算沈陽繞城高速嗎?準確的說,京哈高速全程1209公里。路線:北京-唐山-秦皇島-葫蘆島-錦州-盤錦-沈陽-鐵嶺-四平-長春-哈爾濱。沈陽繞城高速是京哈高速的立交。如果你想去哈爾濱,你應該走北環路。沈陽向西到鐵嶺通遼,在王家溝換乘鐵嶺京哈高速。沈陽繞城高速多少邁?沈陽繞城高速大部分路段限速120公里,部分路段限速100公里,少數路段限速80公里。沈陽繞城高速一般是指沈陽的三環路。是國家批...
全球最浪漫的品牌?感謝Van Cleef amp; amp雅寶的詩意系列,鉆石與機械的魅力完美融合,打造出世界上最浪漫的腕表,——情人橋。在牛郎織女的傳說中,每年七夕只有情侶才能相聚片刻,這座“情人橋”也在上演著同樣的故事。小小的表框里的一對戀人,隨著時間的跳動,一步一步向戀人靠近。他們每天12點只能有一分鐘的時間分開。他們在這座橋上不斷重復著11小時59分鐘的差別,但他們會在最后一刻。范克利...