返回顶部
首页 > 资讯 > 后端开发 > Python >Python技法之简单递归下降Parser的实现方法
  • 794
分享到

Python技法之简单递归下降Parser的实现方法

2024-04-02 19:04:59 794人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录1. 算术运算表达式求值2. 生成表达式树左递归和运算符优先级陷阱3. 相关包参考总结1. 算术运算表达式求值 在上一篇博文《python技法:用re模块实现简易tokenize

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的关系:

在实际的解析过程中,我们对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模块。

参考

  • [1] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O'Reilly Media, Inc.", 2015.
  • [2] https://cs61a.org/study-guide/regex/
  • [3] Https://cs61a.org/study-guide/bnf/
  • [4] https://zh.wikipedia.org/wiki/巴科斯范式

总结

到此这篇关于Python技法之简单递归下降Parser实现的文章就介绍到这了,更多相关Python递归下降Parser内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: Python技法之简单递归下降Parser的实现方法

本文链接: https://lsjlt.com/news/117604.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Python技法之简单递归下降Parser的实现方法
    目录1. 算术运算表达式求值2. 生成表达式树左递归和运算符优先级陷阱3. 相关包参考总结1. 算术运算表达式求值 在上一篇博文《Python技法:用re模块实现简易tokenize...
    99+
    2024-04-02
  • Python递归下降Parser怎么实现
    1. 算术运算表达式求值要解析这类文本,需要另外一种特定的语法规则。我们这里介绍可以表示上下文无关文法(context free grammer)的语法规则巴科斯范式(BNF)和扩展巴科斯范式(EBNF)。从小到一个算术运算表达式,到大到几...
    99+
    2023-05-17
    Python parser
  • python中如何实现递归方法
    小编给大家分享一下python中如何实现递归方法,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!1.递归概念递归是解决问题的一种方法,它将问题不断地分成更小的子问题,直到子问题可以用普通的方法解决。通常情况下,递归会使用一个...
    99+
    2023-06-22
  • Java递归实现菜单树的方法详解
    pom文件 <xml version="1.0" encoding="UTF-8"> <project xmlns="http://maven.apache.or...
    99+
    2024-04-02
  • python二分查找算法的递归实现方法
    本文实例讲述了python二分查找算法的递归实现方法。分享给大家供大家参考,具体如下: 这里先提供一段二分查找的代码: def binarySearch(alist, item): first = ...
    99+
    2022-06-04
    递归 算法 方法
  • Python实现多元线性回归的梯度下降法
    目录1. 读取数据2.定义代价函数3. 梯度下降4.可视化展示1. 读取数据 首先要做的就是读取数据,请自行准备一组适合做多元回归的数据即可。这里以data.csv为例,这里做的是二...
    99+
    2024-04-02
  • python非递归全排列实现方法
    刚刚开始学习python,当前看到了函数这一节。结合数组操作,写了个非递归的全排列生成。原理是插入法,也就是在一个有n个元素的已有排列中,后加入的元素,依次在前,中,后的每一个位置插入,生成n+1个新的全排...
    99+
    2022-06-04
    递归 排列 方法
  • 基于Vue技术实现递归组件的方法
    描述 本文介绍的是基于Vue技术实现递归组件的方法。用Vue实现一级列表、二级列表的展示很简单,但是想要实现无限级,光是套上一个又一个的v-for是行不通的,这个时候就需要用到递归的...
    99+
    2024-04-02
  • mysql实现递归查询的方法
    小编给大家分享一下mysql实现递归查询的方法,希望大家阅读完这篇文章后大所收获,下面让我们一起去探讨吧!mysql实现递归查询的方法:首先创建表,并初始化数据;然后向下递归,利用【find_in_set(...
    99+
    2024-04-02
  • Java排序算法之归并排序简单实现
    算法描述:对于给定的一组记录,首先将每两个相邻的长度为1的子序列进行归并,得到 n/2(向上取整)个长度为2或1的有序子序列,再将其两两归并,反复执行此过程,直到得到一个有序序列。package sorting;public class M...
    99+
    2023-05-30
    java算法 归并排序 ava
  • python排序算法的简单实现方法
    1 冒泡排序  1.1 算法步骤: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元...
    99+
    2024-04-02
  • python递归&迭代方法实现链表反转
    定义链表node结构: class ListNode:       def __init__(self,data):         self.data = data        ...
    99+
    2024-04-02
  • Android简单实现屏幕下方Tab菜单的方法
    本文实例讲述了Android简单实现屏幕下方Tab菜单的方法。分享给大家供大家参考,具体如下: 看到很多热门的Android程序(如:新浪微博、腾讯微博、京东商城、淘宝、当当等...
    99+
    2022-06-06
    方法 tab Android
  • DropDownList显示的C#递归的实现方法
    本篇内容介绍了“DropDownList显示的C#递归的实现方法”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!C#递归实现Drop...
    99+
    2023-06-17
  • Python数据结构之递归可视化的方法
    今天小编给大家分享一下Python数据结构之递归可视化的方法的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.学习目标递归函...
    99+
    2023-06-30
  • JAVA递归生成树形菜单的实现方法是什么
    今天小编给大家分享一下JAVA递归生成树形菜单的实现方法是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。  递归生成一个...
    99+
    2023-06-26
  • Mysql树形递归查询的实现方法
    前言 对于数据库中的树形结构数据,如部门表,有时候,我们需要知道某部门的所有下属部分或者某部分的所有上级部门,这时候就需要用到mysql的递归查询 最近在做项目迁移,Oracle版本的迁到Mysql版本,遇...
    99+
    2024-04-02
  • Golang函数的递归调用实现方法
    Golang函数的递归调用实现方法随着Golang在软件开发中的广泛应用,函数的递归调用成为了程序员们实现复杂逻辑和算法的重要手段。递归调用是指在函数内部不断地调用自身,直至满足某一个条件终止循环。在本文中,我们将探讨Golang函数的递归...
    99+
    2023-05-17
    函数 Golang 递归调用
  • Python实现emoji表情的简单方法
    前言 作为一个数据分析师,应该信奉一句话——“一图胜千言”。不过这里要说的并不是数据可视化,而是一款全民向的产品形态——表情包!!!! 表情包不仅仅是一种符号,更是一种文化——是促进社交乃至社会发展的动力之一,就像懒...
    99+
    2022-06-02
    python emoji表情存储 python表情包 python emoji
  • Python实现简单字典树的方法
    本文实例讲述了Python实现简单字典树的方法。分享给大家供大家参考,具体如下: #coding=utf8 """代码实现了最简单的字典树,只支持由小写字母组成的字符串。 在此代码基础上扩展一下,就可以...
    99+
    2022-06-04
    字典 简单 方法
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作