Python实现简单得递归下降Parser


Posted in Python onMay 02, 2022

1. 算术运算表达式求值

在上一篇博文《Python如何用re模块实现简易tokenizer》中,我们介绍了用正则表达式来匹配对应的模式,以实现简单的分词器。然而,正则表达式不是万能的,它本质上是一种有限状态机(finite state machine,FSM), 无法处理含有递归语法的文本,比如算术运算表达式。

要解析这类文本,需要另外一种特定的语法规则。我们这里介绍可以表示上下文无关文法(context free grammer)的语法规则巴科斯范式(BNF)和扩展巴科斯范式(EBNF)。实际上,小到一个算术运算表达式,大到几乎所有程序设计语言,都是通过上下文无关文法来定义的。

对于简单的算术运算表达式,假定我们已经用分词技术将其转化为输入的tokens流,如NUM+NUM*NUM(分词方法参见上一篇博文)。

在此基础上,我们定义BNF规则定义如下:

expr ::= expr + term
     | expr - term 
     | term
term ::= term * factor
     | term / factor
     | factor
factor ::= (expr)
     | NUM

当然,这种计法还不够简洁明了,我们实际采用的为EBNF形式:

expr ::= term { (+|-) term }*
term ::= factor { (*|/) factor }*
factor ::= (expr) 
       | NUM

BNF和EBNF每一条规则(形如::=的式子)都可以看做是一种替换,即左侧的符号可以被右侧的符号所替换。而解析的过程中我们尝试将输入文本同语法规则做匹配,通过BNF/EBNF来完成各种替换和扩展。其中,EBNF中包含在{...}*中的规则是可选的,*意味着零个或多个重复项(参考正则表达式)。

下图形象地展示了递归下降解析器(parser)中“递归”和“下降”部分和ENBF的关系:

Python实现简单得递归下降Parser

在实际的解析过程中,我们对tokens流从左到右进行扫描,在扫描的过程中处理token,如果卡住就产生一个语法错误。对于规则,我们将每一条语法规则转变为一个函数或方法,比如上面的ENBF规则就转换为下列的方法:

class ExpressionEvaluator():
    ...
    def expr(self):
        ...
    def term(self):
        ...
    def factor(self):
        ...

在调用某个规则对应方法的过程中,如果我们发现接下来的符号需要采用另一个规则来匹配,则我们就会“下降”到另一个规则方法(如在expr中调用term,term中调用factor),则也就是递归下降中“下降”的部分。

有时也会调用已经在执行的方法(比如在expr中调用term,term中调用factor后,又在factor中调用expr,相当于一条衔尾蛇),这也就是递归下降中“递归”的部分。

对于语法中出现的重复部分(例如expr ::= term { (+|-) term }*),我们则通过while循环来实现。

下面我们来看具体的代码实现。首先是分词部分,我们参照上一篇介绍分词博客的代码。

import re
import collections

# 定义匹配token的模式
NUM = r'(?P<NUM>\d+)'  # \d表示匹配数字,+表示任意长度
PLUS = r'(?P<PLUS>\+)'  # 注意转义
MINUS = r'(?P<MINUS>-)'
TIMES = r'(?P<TIMES>\*)'  # 注意转义
DIVIDE = r'(?P<DIVIDE>/)'
LPAREN = r'(?P<LPAREN>\()'  # 注意转义
RPAREN = r'(?P<RPAREN>\))'  # 注意转义
WS = r'(?P<WS>\s+)'  # 别忘记空格,\s表示空格,+表示任意长度

master_pat = re.compile(
    '|'.join([NUM, PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN, WS]))

# Tokenizer
Token = collections.namedtuple('Token', ['type', 'value'])


def generate_tokens(text):
    scanner = master_pat.scanner(text)
    for m in iter(scanner.match, None):
        tok = Token(m.lastgroup, m.group())
        if tok.type != 'WS':  # 过滤掉空格符
            yield tok

下面是表达式求值器的具体实现:

class ExpressionEvaluator():
    """ 递归下降的Parser实现,每个语法规则都对应一个方法,
    使用 ._accept()方法来测试并接受当前处理的token,不匹配不报错,
    使用 ._except()方法来测试当前处理的token,并在不匹配的时候抛出语法错误
    """

    def parse(self, text):
        """ 对外调用的接口 """
        self.tokens = generate_tokens(text)
        self.tok, self.next_tok = None, None  # 已匹配的最后一个token,下一个即将匹配的token
        self._next()  # 转到下一个token
        return self.expr()  # 开始递归

    def _next(self):
        """ 转到下一个token """
        self.tok, self.next_tok = self.next_tok, next(self.tokens, None)

    def _accept(self, tok_type):
        """ 如果下一个token与tok_type匹配,则转到下一个token """
        if self.next_tok and self.next_tok.type == tok_type:
            self._next()
            return True
        else:
            return False

    def _except(self, tok_type):
        """ 检查是否匹配,如果不匹配则抛出异常 """
        if not self._accept(tok_type):
            raise SyntaxError("Excepted"+tok_type)

    # 接下来是语法规则,每个语法规则对应一个方法
    
    def expr(self):
        """ 对应规则: expression ::= term { ('+'|'-') term }* """
        exprval = self.term() # 取第一项
        while self._accept("PLUS") or self._accept("DIVIDE"): # 如果下一项是"+"或"-"
            op = self.tok.type 
            # 再取下一项,即运算符右值
            right = self.term() 
            if op == "PLUS":
                exprval += right
            elif op == "MINUS":
                exprval -= right
        return exprval
            
    def term(self):
        """ 对应规则: term ::= factor { ('*'|'/') factor }* """
        
        termval = self.factor() # 取第一项
        while self._accept("TIMES") or self._accept("DIVIDE"): # 如果下一项是"+"或"-"
            op = self.tok.type 
            # 再取下一项,即运算符右值
            right = self.factor() 
            if op == "TIMES":
                termval *= right
            elif op == "DIVIDE":
                termval /= right
        return termval          
            
        
    def factor(self):
        """ 对应规则: factor ::= NUM | ( expr ) """
        if self._accept("NUM"): # 递归出口
            return int(self.tok.value)
        elif self._accept("LPAREN"):
            exprval = self.expr() # 继续递归下去求表达式值
            self._except("RPAREN") # 别忘记检查是否有右括号,没有则抛出异常
            return exprval
        else:
            raise SyntaxError("Expected NUMBER or LPAREN")

我们输入以下表达式进行测试:

e = ExpressionEvaluator()
print(e.parse("2"))
print(e.parse("2+3"))
print(e.parse("2+3*4"))
print(e.parse("2+(3+4)*5"))

求值结果如下:

2
5
14
37

如果我们输入的文本不符合语法规则:

print(e.parse("2 + (3 + * 4)"))

则会抛出SyntaxError异常:Expected NUMBER or LPAREN
综上,可见我们的表达式求值算法运行正确。

2. 生成表达式树

上面我们是得到表达式的结果,但是如果我们想分析表达式的结构,生成一棵简单的表达式解析树呢?那么我们需要对上述类的方法做一定修改:

class ExpressionTreeBuilder(ExpressionEvaluator):
    def expr(self):
            """ 对应规则: expression ::= term { ('+'|'-') term }* """
            exprval = self.term() # 取第一项
            while self._accept("PLUS") or self._accept("DIVIDE"): # 如果下一项是"+"或"-"
                op = self.tok.type 
                # 再取下一项,即运算符右值
                right = self.term() 
                if op == "PLUS":
                    exprval = ('+', exprval, right)
                elif op == "MINUS":
                    exprval -= ('-', exprval, right)
            return exprval
    
    def term(self):
        """ 对应规则: term ::= factor { ('*'|'/') factor }* """
        
        termval = self.factor() # 取第一项
        while self._accept("TIMES") or self._accept("DIVIDE"): # 如果下一项是"+"或"-"
            op = self.tok.type 
            # 再取下一项,即运算符右值
            right = self.factor() 
            if op == "TIMES":
                termval = ('*', termval, right)
            elif op == "DIVIDE":
                termval = ('/', termval, right)
        return termval          
    
    def factor(self):
        """ 对应规则: factor ::= NUM | ( expr ) """
        if self._accept("NUM"): # 递归出口
            return int(self.tok.value) # 字符串转整形
        elif self._accept("LPAREN"):
            exprval = self.expr() # 继续递归下去求表达式值
            self._except("RPAREN") # 别忘记检查是否有右括号,没有则抛出异常
            return exprval
        else:
            raise SyntaxError("Expected NUMBER or LPAREN")

输入下列表达式测试一下:

print(e.parse("2+3"))
print(e.parse("2+3*4"))
print(e.parse("2+(3+4)*5"))
print(e.parse('2+3+4'))

以下是生成结果:

('+', 2, 3)
('+', 2, ('*', 3, 4))
('+', 2, ('*', ('+', 3, 4), 5))
('+', ('+', 2, 3), 4)

可以看到表达式树生成正确。

我们上面的这个例子非常简单,但递归下降的解析器也可以用来实现相当复杂的解析器,例如Python代码就是通过一个递归下降解析器解析的。您要是对此跟感兴趣可以检查Python源码中的Grammar文件来一探究竟。然而,下面我们接着会看到,自己动手写一个解析器会面对各种陷阱和挑战。

左递归和运算符优先级陷阱

任何涉及左递归形式的语法规则,都没法用递归下降parser来解决。所谓左递归,即规则式子右侧最左边的符号是规则头,比如对于以下规则:

items ::= items ',' item 
      | item

完成该解析你可能会定义以下方法:

def items(self):
    itemsval = self.items() # 取第一项,然而此处会无穷递归!
    if itemsval and self._accept(','):
        itemsval.append(self.item())
    else:
        itemsval = [self.item()]

这样做会在第一行就无穷地调用self.items()从而产生无穷递归错误。

还有一种是语法规则自身的错误,比如运算符优先级。我们如果忽视运算符优先级直接将表达式简化如下:

expr ::= factor { ('+'|'-'|'*'|'/') factor }*
factor ::= '(' expr ')'
       | NUM
PYTHON 复制 全屏

这个语法从技术上可以实现,但是没有遵守计算顺序约定,导致"3+4*5"的运算结果为35,而不是预期的23。故此处需要用独立的expr和term规则来确保计算结果的正确性。

3. 相关包

最后,对于真正复杂的语法解析,建议采用PyParsing或PLY这样的解析工具。如果你对Python代码的抽象语法树感兴趣,可以看下Python自带的ast模块。

参考

总结

到此这篇关于Python实现简单得递归下降Parser的文章就介绍到这了!


Tags in this post...

Python 相关文章推荐
动态创建类实例代码
Oct 07 Python
win7上python2.7连接mysql数据库的方法
Jan 14 Python
Python 十六进制整数与ASCii编码字符串相互转换方法
Jul 09 Python
Python常见内置高效率函数用法示例
Jul 31 Python
python 3.7.0 下pillow安装方法
Aug 27 Python
python3.6利用pyinstall打包py为exe的操作实例
Oct 31 Python
python 使用 requests 模块发送http请求 的方法
Dec 09 Python
Python 异常处理Ⅳ过程图解
Oct 18 Python
通过实例了解Python str()和repr()的区别
Jan 17 Python
如何在scrapy中集成selenium爬取网页的方法
Nov 18 Python
Jupyter Notebook添加代码自动补全功能的实现
Jan 07 Python
图神经网络GNN算法
May 11 Python
使用Python开发贪吃蛇游戏 SnakeGame
Apr 30 #Python
使用Python开发冰球小游戏
详解Python中的for循环
Python采集壁纸并实现炫轮播
Apr 30 #Python
Python循环之while无限迭代
如何Python使用re模块实现okenizer
Apr 30 #Python
如何使用python包中的sched事件调度器
Apr 30 #Python
You might like
dedecms模板标签代码官方参考
2007/03/17 PHP
PHP字符串处理的10个简单方法
2010/06/30 PHP
php 二维数组时间排序实现代码
2016/11/19 PHP
PHP数组去重的更快实现方式分析
2018/05/09 PHP
BOOM vs RR BO3 第二场2.13
2021/03/10 DOTA
javascript编程起步(第七课)
2007/02/27 Javascript
jquery插件制作 图片走廊 gallery
2012/08/17 Javascript
Jquery中给animation加更多的运作效果实例
2013/09/05 Javascript
JavaScript中的alert()函数使用技巧详解
2014/12/29 Javascript
JavaScript中的闭包(Closure)详细介绍
2014/12/30 Javascript
Windows系统下使用Sublime搭建nodejs环境
2015/04/13 NodeJs
学习JavaScript正则表达式
2015/11/13 Javascript
跟我学习javascript的arguments对象
2015/11/16 Javascript
基于Bootstrap实现的下拉菜单手机端不能选择菜单项的原因附解决办法
2016/07/22 Javascript
AngularJS ng-bind-template 指令详解
2016/07/30 Javascript
JS实现图文并茂的tab选项卡效果示例【附demo源码下载】
2016/09/21 Javascript
jquery attr()设置和获取属性值实例教程
2016/09/25 Javascript
bootstrap输入框组件使用方法详解
2017/01/19 Javascript
jquery实现自适应banner焦点图
2017/02/16 Javascript
angularJS模态框$modal实例代码
2017/05/27 Javascript
基于Vue自定义指令实现按钮级权限控制思路详解
2018/05/23 Javascript
node.js开发辅助工具nodemon安装与配置详解
2020/02/06 Javascript
python字符串连接方式汇总
2014/08/21 Python
python使用opencv进行人脸识别
2017/04/07 Python
python画出三角形外接圆和内切圆的方法
2018/01/25 Python
Python 批量合并多个txt文件的实例讲解
2018/05/08 Python
python爬虫 2019中国好声音评论爬取过程解析
2019/08/26 Python
Python 内置函数globals()和locals()对比详解
2019/12/23 Python
怎么快速自学python
2020/06/22 Python
Python性能分析工具py-spy原理用法解析
2020/07/27 Python
Django DRF APIView源码运行流程详解
2020/08/17 Python
详解Selenium-webdriver绕开反爬虫机制的4种方法
2020/10/28 Python
华为菲律宾官方网站:HUAWEI Philippines
2021/02/23 全球购物
领导干部四风问题自我剖析材料
2014/09/25 职场文书
MySQL之PXC集群搭建的方法步骤
2021/05/25 MySQL
Windows11里微软已经将驱动程序安装位置A盘删除
2021/11/21 数码科技