再谈递归下降解析器:构建一个简单的算术表达式解析器

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 本文介绍了递归下降解析器的原理与实现,重点讲解了如何使用Python构建一个简单的算术表达式解析器。通过定义文法、实现词法分析器和解析器类,最终实现了对基本算术表达式的解析与计算功能。

再谈递归下降解析器:构建一个简单的算术表达式解析器

引言

在编译原理中,解析是将源代码转换为抽象语法树(AST)的过程。这一过程对于任何编程语言的实现都是至关重要的一步。递归下降解析是一种自顶向下的解析策略,它直接从文法开始构建解析函数。每个非终结符都有对应的解析函数,这些函数按照文法规则调用其他函数(包括自身),从而形成递归结构。本文将介绍如何使用Python实现一个简单的递归下降解析器,用于解析和计算基本的算术表达式。

什么是递归下降解析?

递归下降解析是一种自顶向下的解析方法,它通过为文法中的每个非终结符编写一个对应的解析函数来实现。这些函数根据文法规则递归地调用其他函数,从而逐步解析输入字符串。递归下降解析特别适合于LL(k)类型的文法,其中k表示向前查看的符号数目。

特点

  • 易于理解和实现:因为其结构直观地反映了文法定义。
  • 支持左递归:通过一些技巧可以处理大多数形式的左递归问题。
  • 灵活性高:可以根据需要调整错误处理机制或添加额外的功能。

构建一个简单的算术表达式解析器

接下来,我们将使用Python语言来实现一个能够解析简单算术表达式的递归下降解析器。这里考虑的算术表达式仅包含加减乘除运算以及括号。

文法定义

首先定义我们的文法如下:

  • expr -> term ((PLUS | MINUS) term)*
  • term -> factor ((MUL | DIV) factor)*
  • factor -> NUMBER | (expr)

这里的PLUS, MINUS, MUL, DIV分别代表加、减、乘、除操作;NUMBER代表数字。

Python 实现

1. 定义正则表达式和Token

我们首先定义正则表达式来匹配各种符号,并使用collections.namedtuple来创建Token对象。

import re
import collections

NUM = r'(?P<NUM>\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+)'

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

2. 解析器类

接下来,我们定义一个解析器类ExpressionEvaluator,该类实现了递归下降解析的方法。

class ExpressionEvaluator:
    '''
    Implementation of a recursive descent parser. Each method implements a single grammar rule.
    Use the ._accept() method to test and accept the current lookahead token. Use the ._expect()
    method to exactly match and discard the next token on on the input or raise a SyntaxError if it doesn't match.
    '''

    def parse(self, text):
        self.tokens = generate_tokens(text)
        self.tok = None
        self.nexttok = None
        self._advance()
        return self.expr()

    def _advance(self):
        'Advance one token ahead'
        self.tok, self.nexttok = self.nexttok, next(self.tokens, None)

    def _accept(self, toktype):
        'Test and consume the next token if it matches toktype'
        if self.nexttok and self.nexttok.type == toktype:
            self._advance()
            return True
        else:
            return False

    def _expect(self, toktype):
        'Consume next token if it matches toktype or raise SyntaxError'
        if not self._accept(toktype):
            raise SyntaxError('Expected ' + toktype)

    # Grammar rules follow

    def expr(self):
        "expression ::= term { ('+'|'-') term }*"

        exprval = self.term()
        while self._accept('PLUS') or self._accept('MINUS'):
            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._expect('RPAREN')
            return exprval
        else:
            raise SyntaxError('Expected NUMBER or LPAREN')

使用示例

最后,我们可以使用这个解析器来解析和计算一些简单的算术表达式。

e = ExpressionEvaluator()
print(e.parse('2'))  # 输出: 2
print(e.parse('2 + 3'))  # 输出: 5
print(e.parse('2 + (3 + 4) * 5'))  # 输出: 37
try:
    print(e.parse('2 + (3 + * 4)'))  # 应该抛出SyntaxError
except SyntaxError as e:
    print(f"Syntax Error: {e}")

结论

递归下降解析提供了一种非常直观的方式来实现解析器,尤其适用于教育目的和小型项目。虽然它的效率可能不如某些专门设计的解析算法如LALR(1),但对于许多应用场景来说已经足够强大了。通过本文提供的示例代码,你可以轻松地理解并实现一个简单的递归下降解析器,用于解析和计算基本的算术表达式。希望本文能帮助你掌握这项技术!

欢迎点赞、关注、转发、收藏!!!

相关文章
|
6月前
|
Rust 前端开发 编译器
Python 之父的解析器系列之七:PEG 解析器的元语法
Python 之父的解析器系列之七:PEG 解析器的元语法
31 0
|
6月前
|
存储 缓存 数据可视化
Python 之父的解析器系列之三:生成一个 PEG 解析器
Python 之父的解析器系列之三:生成一个 PEG 解析器
70 0
|
JavaScript
【Vue2.0源码学习】模板编译篇-模板解析阶段(文本解析器)
【Vue2.0源码学习】模板编译篇-模板解析阶段(文本解析器)
75 0
|
6月前
|
缓存 前端开发 Java
视图映射掌握:解析Spring MVC视图解析器的全方位指南
视图映射掌握:解析Spring MVC视图解析器的全方位指南
137 1
|
6月前
|
存储 前端开发 Java
Springboot使用参数解析器HandlerMethodArgumentResolver,解析请求头里的数据
HandlerMethodArgumentResolver 是 Spring MVC 中的一个接口,它允许你自定义方法参数的解析过程。当处理请求时,Spring MVC 需要将请求中的信息映射到控制器方法的参数上,而 HandlerMethodArgumentResolver 允许你在这个过程中进行自定义操作。
179 2
|
Ubuntu Shell Linux
Shell脚本的常用执行方式、bash 和 sh 的关系、子shell、Centos 默认的解析器是 bash、Linux 提供的 Shell 解析器、Shell 概述、Shell 脚本入门
采用 bash 或 sh+脚本的相对路径或绝对路径(不用赋予脚本+x 权限)、采用输入脚本的绝对路径或相对路径执行脚本(必须具有可执行权限+x)、在脚本的路径前加上“.”或者 source(了解)原因: 前两种方式都是在当前 shell 中打开一个子 shell 来执行脚本内容,当脚本内容结束,则 子 shell 关闭,回到父 shell 中。第三种,也就是使用在脚本路径前加“.”或者 source 的方式,`可以使脚本内容在当前 shell 里执行,而无需打开子 shell!`这也是为什么我们每次要修改完
1118 1
Shell脚本的常用执行方式、bash 和 sh 的关系、子shell、Centos 默认的解析器是 bash、Linux 提供的 Shell 解析器、Shell 概述、Shell 脚本入门
|
存储 自然语言处理 JavaScript
【Vue2.0源码学习】模板编译篇-模板解析阶段(HTML解析器)
【Vue2.0源码学习】模板编译篇-模板解析阶段(HTML解析器)
68 0
|
自然语言处理 PHP 数据安全/隐私保护
带你写一个Mp文件解析器-Mp3文件结构全解析(一)
ID3V2一共有四个版本,ID3V2.1/2.2/2.3/2.4,目前流行的播放软件一般只支持第三版即ID3V2.3,由于ID3V1记录在文件的末尾处,ID3V2就只能记录在文件的首部了,也是因为这个原因,对ID3V2的操作比ID3V1要慢,而且ID3V2的结构比ID3V1的结构复杂的多,但是ID3V2可以记录更多的信息,长度可变
23309 4
|
算法 索引
带你写一个Mp文件解析器-Mp3文件结构全解析(二)
每个FRAME 都有一个帧头FRAMEHEADER,长度是4BYTE(32bit),帧头后面可能有两个字节的CRC 校验,这两个字节的是否存在决定于FRAMEHEADER 信息的第16bit, 为0 则帧头后面无校验,为1 则有校验,校验值长度为2 个字节,紧跟在FRAMEHEADER 后面,接着就是帧的实体数据了
213 0

推荐镜像

更多
下一篇
无影云桌面