基于Python編寫一個(gè)解析器
前言
先前,我們實(shí)現(xiàn)了一個(gè)基本的詞法分析器。那么現(xiàn)在的話,我們要在這個(gè)基礎(chǔ)上面,實(shí)現(xiàn)一個(gè)解析器,那么實(shí)現(xiàn)效果的話,是這樣的:
注意此時(shí)此刻,我們還啥都沒有,目前我們還沒有自己的變量等等,我們目前還是只是對 數(shù)學(xué)運(yùn)算進(jìn)行處理并且只是對+-*/進(jìn)行簡單處理
。當(dāng)然這里的話,對比上次做的是做出了一點(diǎn)改動(dòng),當(dāng)然這都不是重點(diǎn),因?yàn)樵谶@里都會進(jìn)行說明。
語法樹
由于,我們在這里的話,我們要做一個(gè)解析器,因此的話,我們毫無疑問的是,我們需要去構(gòu)建一個(gè)簡單的語法樹。那么在這里的話,先前,我們是實(shí)現(xiàn)了一個(gè)簡單的詞法解析器,也就是我們把這些東西都解析為了Token。
當(dāng)然為了適應(yīng)這部分的代碼,我們對先前的代碼進(jìn)行了一點(diǎn)改動(dòng)。至于是哪里改動(dòng)了,這里會進(jìn)行說明(由于代碼不難超過文章的60%,所以這里無法貼出全部代碼,后期做好了,再給倉庫地址吧~
那么在這里的話,我們看到了剛剛的效果,所以的話,我們這里的話其實(shí)還是只是實(shí)現(xiàn)了一個(gè)簡單的運(yùn)算表達(dá)式的語法解析。
也就是其實(shí),我們今天實(shí)現(xiàn)的效果是這樣的,假設(shè)用戶在終端輸入:
在我們的代碼里面將構(gòu)造出這個(gè):
換一句話說,我們今天實(shí)現(xiàn)的效果,其實(shí)就是中綴表達(dá)式用一顆樹來表示。只是這個(gè)樹,在我們這里叫做語法樹。目前我們只是解析這個(gè),后面我們再解析變量等等,這樣的話,我們就可以非常愉快地實(shí)現(xiàn)這個(gè)小語言了。
所以在這塊我們先引入幾個(gè)東西。
節(jié)點(diǎn)
在這里的話,我們要做的是把剛剛的這個(gè)轉(zhuǎn)化為一顆語法樹,也就是(目前)輸入的合法的數(shù)學(xué)表達(dá)式。所以的話,為了能夠去構(gòu)建這棵樹,我們定義了如下節(jié)點(diǎn):
""" 定義節(jié)點(diǎn): NumberNode:表示數(shù)字節(jié)點(diǎn),用于存儲數(shù)字的令牌(token)信息。 BinOpNode:表示二元操作符節(jié)點(diǎn)。其中: left_node(左節(jié)點(diǎn)), op_tok(操作符令牌), right_node(右節(jié)點(diǎn)) UnaryOpNode:表示一元操作符節(jié)點(diǎn)。 op_tok(操作符令牌) node(子節(jié)點(diǎn)) """ class NumberNode: def __init__(self, tok): self.tok = tok def __repr__(self): return f'{self.tok}' class BinOpNode: def __init__(self, left_node, op_tok, right_node): self.left_node = left_node self.op_tok = op_tok self.right_node = right_node def __repr__(self): return f'({self.left_node}, {self.op_tok}, {self.right_node})' class UnaryOpNode: def __init__(self, op_tok, node): self.op_tok = op_tok self.node = node def __repr__(self): return f'({self.op_tok}, {self.node})'
語法描述
接下來,我們把我們提到的這個(gè)節(jié)點(diǎn)和我的剛剛的節(jié)點(diǎn)進(jìn)行對應(yīng),進(jìn)行查抽象, 并且他們的代碼關(guān)系大致是這樣的:
于是得到語法描述
expr(表達(dá)式):由一個(gè)項(xiàng)(term)后面跟隨零個(gè)或多個(gè)類似于加法(PLUS)或減法(MINUS)操作符的項(xiàng)組成。
term(項(xiàng)):由一個(gè)因子(factor)后面跟隨零個(gè)或多個(gè)類似于乘法(MUL)或除法(DIV)操作符的因子組成。
factor(因子)> :可以是整數(shù)(INT)或浮點(diǎn)數(shù)(FLOAT) > :是一個(gè)加法(PLUS)或減法(MINUS)操作符后面跟隨一個(gè)因子 > :或者是括號內(nèi)的一個(gè)表達(dá)式(expr)。
異常處理
okey,在開始我們的具體核心實(shí)現(xiàn)之前的話,我們還是來看到我們對異常的處理吧。
InvalidSyntaxError
那么在這里我們首當(dāng)其沖的就是我們的這個(gè)語法錯(cuò)誤的報(bào)錯(cuò)。
class InvalidSyntaxError(HlangError): def __init__(self, pos_start, pos_end, details=''): super().__init__(pos_start, pos_end, 'Invalid Syntax:語法錯(cuò)誤', details)
也是我們在學(xué)習(xí)過程當(dāng)中,遇到的第一個(gè)錯(cuò)誤。
那么在這里的話,對比上次的代碼,在這里我對Error父類也進(jìn)行了改動(dòng):
class HlangError: def __init__(self, pos_start, pos_end, error_name, details): """ :param pos_start: 錯(cuò)誤開始未知 :param pos_end: 錯(cuò)誤結(jié)束未知 :param error_name: 錯(cuò)位類型 :param details: 細(xì)節(jié) """ self.pos_start = pos_start self.pos_end = pos_end self.error_name = error_name self.details = details def as_string(self): red_code = "\033[91m" reset_code = "\033[0m" result = f'{self.error_name}: {self.details}\n' result += f'File {self.pos_start.fn}, line {self.pos_start.ln + 1}' result += '\n\n' + string_with_arrows(self.pos_start.ftxt, self.pos_start, self.pos_end) return red_code+result+reset_code
錯(cuò)誤內(nèi)容定位
那么在這里的話,我之所以做這個(gè)改動(dòng),主要是因?yàn)?,我們要?shí)現(xiàn)這個(gè)顯示錯(cuò)誤的內(nèi)容提示,那么這個(gè)時(shí)候的話,當(dāng)出現(xiàn)異常的時(shí)候,之所以,可以這樣,還有一個(gè)原因是,我們對這個(gè)類Position也進(jìn)行了修改,這個(gè)家伙,現(xiàn)在會記錄當(dāng)前的文本了:
class Position: def __init__(self, idx, ln, col, fn, ftxt): self.idx = idx self.ln = ln self.col = col self.fn = fn self.ftxt = ftxt def advance(self, current_char=None): self.idx += 1 self.col += 1 if current_char == '\n': self.ln += 1 self.col = 0 return self def copy(self): return Position(self.idx, self.ln, self.col, self.fn, self.ftxt)
這樣做的好處是能夠進(jìn)行快速定位顯示錯(cuò)誤。
之后的話,為了顯示出錯(cuò)誤:
這里有一個(gè)專門用來顯示錯(cuò)誤的方法:
def string_with_arrows(text, pos_start, pos_end): result = '' # 計(jì)算索引位置 idx_start = max(text.rfind('\n', 0, pos_start.idx), 0) idx_end = text.find('\n', idx_start + 1) if idx_end < 0: idx_end = len(text) # 生成每一行 line_count = pos_end.ln - pos_start.ln + 1 for i in range(line_count): # 計(jì)算行的列數(shù) line = text[idx_start:idx_end] col_start = pos_start.col if i == 0 else 0 col_end = pos_end.col if i == line_count - 1 else len(line) - 1 # 添加到結(jié)果中 result += line + '\n' result += ' ' * col_start + '^' * (col_end - col_start) # 重新計(jì)算索引位置 idx_start = idx_end idx_end = text.find('\n', idx_start + 1) if idx_end < 0: idx_end = len(text) return result.replace('\t', '')
解析實(shí)現(xiàn)
那么在這里之后的話,就是我們解析的實(shí)現(xiàn)了。
那么首先在這里的話,我們對這個(gè)節(jié)點(diǎn)在進(jìn)行一個(gè)簡單的封裝:
class ParseResult: def __init__(self): self.error = None self.node = None def register(self, res): if isinstance(res, ParseResult): if res.error: self.error = res.error return res.node return res def success(self, node): self.node = node return self def failure(self, error): self.error = error return self
這樣做的目的只要是為了,能夠去記錄一下異常。記錄一下這個(gè)報(bào)錯(cuò),不記錄不行。
它的核心其實(shí)就是這幾個(gè)方法:
那么在這里的話主要有兩個(gè)比較核心的代碼:
bin_op函數(shù)
因?yàn)槲覀冞@里要構(gòu)造的就是一棵樹,所以i的話,我們要遞歸處理,我們要去合并左右子樹。所以這個(gè)函數(shù)的作用就是去生成當(dāng)前的語法解析樹。
def bin_op(self, func, ops): res = ParseResult() #構(gòu)造左邊的解析樹 left = res.register(func()) if res.error: return res while self.current_tok.type in ops: #如果,當(dāng)前的token,是token,令牌是這個(gè)符合要求的運(yùn)算符 #那么拿到下一個(gè)token,然后看看能不能構(gòu)造左邊的解析樹 #記住我們這里是中綴表達(dá)式,從左到右的 op_tok = self.current_tok res.register(self.advance()) right = res.register(func()) if res.error: return res #此時(shí)我們把左右合并,組裝成新的樹,但是如果沒有, # 那么左子樹就是當(dāng)前的樹,不用構(gòu)造,沒有新的層級 left = BinOpNode(left, op_tok, right) return res.success(left)
factory函數(shù)
之后是這個(gè)函數(shù),這個(gè)函數(shù)的唯一作用其實(shí)就是構(gòu)造當(dāng)前的節(jié)點(diǎn)。這里的話有一點(diǎn)要說明的是,那就是我們先解析的token是封裝好的,然后我們是按照從左到右進(jìn)行拿到的,我們讀取的時(shí)候也是按照順序讀取的,因此這個(gè)就意味著在我們進(jìn)行解析的時(shí)候,針對這個(gè)數(shù)學(xué)表達(dá)式,其實(shí)就是按照中綴的順序來進(jìn)行處理 的。
def factor(self): res = ParseResult() tok = self.current_tok if tok.type in (TT_PLUS, TT_MINUS): #如果是運(yùn)算符號,那么它是一元操作符,找到下一個(gè)數(shù)字 res.register(self.advance()) factor = res.register(self.factor()) if res.error: return res #返回到右子樹 return res.success(UnaryOpNode(tok, factor)) elif tok.type in (TT_INT, TT_FLOAT): #如果是一個(gè)數(shù)字,那么直接返回節(jié)點(diǎn) res.register(self.advance()) return res.success(NumberNode(tok)) #如果是(,那么說明這個(gè)玩意又是一個(gè)表達(dá)式 elif tok.type == TT_LPAREN: res.register(self.advance()) expr = res.register(self.expr()) if res.error: return res if self.current_tok.type == TT_RPAREN: res.register(self.advance()) return res.success(expr) else: return res.failure(InvalidSyntaxError( self.current_tok.pos_start, self.current_tok.pos_end, "應(yīng)該是: ')'" )) return res.failure(InvalidSyntaxError( tok.pos_start, tok.pos_end, "應(yīng)為:整型或浮點(diǎn)型" ))
模擬流程
如果你熟悉遞歸的話,那么這個(gè)案例,你就不需要看了。怎么去想這個(gè)遞歸的話,給點(diǎn)提示就是,按照不同的類型,去構(gòu)建不同的節(jié)點(diǎn)。然后遞歸只要關(guān)系當(dāng)前的函數(shù)的作用即可。具體的,遞歸思想可以看到我總結(jié)的7W藍(lán)橋杯算法的部分。里面關(guān)于遞歸的寫法是我總結(jié)了的,按照那個(gè)套路寫,leetcode分分鐘秒殺大部分中檔題目,hard 題目那就需要點(diǎn)技巧了。
假設(shè)要解析的表達(dá)式是`(5+3)*2`,那么我們這個(gè)解析器的流程是這樣的: 目標(biāo)是這個(gè): * / \ + 2 / \ 5 3 1. 表達(dá)式:`(5+3)*2` 2. 調(diào)用 `expr()` 方法開始解析表達(dá)式。 3. 在 `bin_op()` 方法內(nèi)部,調(diào)用 `term()` 方法解析左操作數(shù)。 - 調(diào)用 `term()` 方法開始解析項(xiàng)。 - 在 `bin_op()` 方法內(nèi)部,調(diào)用 `factor()` 方法解析左操作數(shù)。 - 獲取數(shù)字 `5` 的令牌,并創(chuàng)建一個(gè) NumberNode 對象表示該數(shù)字。 - 返回左操作數(shù)的解析結(jié)果。 4. 循環(huán)檢查當(dāng)前令牌,發(fā)現(xiàn)下一個(gè)令牌是 `+` 號。 5. 調(diào)用 `bin_op()` 方法解析加法運(yùn)算符。 - 調(diào)用 `term()` 方法作為參數(shù)傳遞給 `bin_op()` 方法,解析右操作數(shù)。 - 調(diào)用 `term()` 方法開始解析項(xiàng)。 - 在 `bin_op()` 方法內(nèi)部,調(diào)用 `factor()` 方法解析右操作數(shù)。 - 獲取數(shù)字 `3` 的令牌,并創(chuàng)建一個(gè) NumberNode 對象表示該數(shù)字。 - 返回右操作數(shù)的解析結(jié)果。 - 創(chuàng)建一個(gè) BinOpNode 對象,表示加法運(yùn)算,左操作數(shù)為前面解析得到的 NumberNode 對象,右操作數(shù)為剛剛解析得到的數(shù)字 `3` 的 NumberNode 對象。 6. 返回加法運(yùn)算的結(jié)果,即 BinOpNode 對象。 7. 循環(huán)檢查當(dāng)前令牌,發(fā)現(xiàn)下一個(gè)令牌是 `*` 號。 8. 調(diào)用 `bin_op()` 方法解析乘法運(yùn)算符。 - 調(diào)用 `term()` 方法作為參數(shù)傳遞給 `bin_op()` 方法,解析右操作數(shù)。 - 獲取數(shù)字 `2` 的令牌,并創(chuàng)建一個(gè) NumberNode 對象表示該數(shù)字。 - 創(chuàng)建一個(gè) BinOpNode 對象,表示乘法運(yùn)算,左操作數(shù)為前面解析得到的 BinOpNode 對象,右操作數(shù)為剛剛解析得到的數(shù)字 `2` 的 NumberNode 對象。 9. 返回乘法運(yùn)算的結(jié)果,即 BinOpNode 對象。 10. 返回表達(dá)式的解析結(jié)果,即最終得到的抽象語法樹的根節(jié)點(diǎn)。
以上就是基于Python編寫一個(gè)解析器的詳細(xì)內(nèi)容,更多關(guān)于Python解析器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python+pywinauto+lackey實(shí)現(xiàn)PC端exe自動(dòng)化的示例代碼
這篇文章主要介紹了python+pywinauto+lackey實(shí)現(xiàn)PC端exe自動(dòng)化的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04python列表刪除元素的三種實(shí)現(xiàn)方法
本文主要介紹了python列表刪除元素的三種實(shí)現(xiàn)方法,主要包括pop方法,remove方法,del方法這三種,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01