## 问题
你想根据一组语法规则解析文本并执行命令,或者构造一个代表输入的抽象语法树。如果语法非常简单,你可以自己写这个解析器,而不是使用一些框架。
## 解决方案
在这个问题中,我们集中讨论根据特殊语法去解析文本的问题。为了这样做,你首先要以BNF或者EBNF形式指定一个标准语法。比如,一个简单数学表达式语法可能像下面这样:
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
在EBNF中,被包含在 `{...}*` 中的规则是可选的。[*](#)代表0次或多次重复(跟正则表达式中意义是一样的)。
现在,如果你对BNF的工作机制还不是很明白的话,就把它当做是一组左右符号可相互替换的规则。一般来讲,解析的原理就是你利用BNF完成多个替换和扩展以匹配输入文本和语法规则。为了演示,假设你正在解析形如 `3 + 4 * 5` 的表达式。这个表达式先要通过使用2.18节中介绍的技术分解为一组令牌流。结果可能是像下列这样的令牌序列:
NUM + NUM * NUM
在此基础上, 解析动作会试着去通过替换操作匹配语法到输入令牌:
expr
expr ::= term { (+|-) term }*
expr ::= factor { (*|/) factor }* { (+|-) term }*
expr ::= NUM { (*|/) factor }* { (+|-) term }*
expr ::= NUM { (+|-) term }*
expr ::= NUM + term { (+|-) term }*
expr ::= NUM + factor { (*|/) factor }* { (+|-) term }*
expr ::= NUM + NUM { (*|/) factor}* { (+|-) term }*
expr ::= NUM + NUM * factor { (*|/) factor }* { (+|-) term }*
expr ::= NUM + NUM * NUM { (*|/) factor }* { (+|-) term }*
expr ::= NUM + NUM * NUM { (+|-) term }*
expr ::= NUM + NUM * NUM
下面所有的解析步骤可能需要花点时间弄明白,但是它们原理都是查找输入并试着去匹配语法规则。第一个输入令牌是NUM,因此替换首先会匹配那个部分。一旦匹配成功,就会进入下一个令牌+,以此类推。当已经确定不能匹配下一个令牌的时候,右边的部分(比如 `{ (*/) factor }*` )就会被清理掉。在一个成功的解析中,整个右边部分会完全展开来匹配输入令牌流。
有了前面的知识背景,下面我们举一个简单示例来展示如何构建一个递归下降表达式求值程序:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Topic: 下降解析器
Desc :
"""
import re
import collections
# Token specification
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
# Parser
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 # Last symbol consumed
self.nexttok = None # Next symbol tokenized
self._advance() # Load first lookahead token
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')
def descent_parser():
e = ExpressionEvaluator()
print(e.parse('2'))
print(e.parse('2 + 3'))
print(e.parse('2 + 3 * 4'))
print(e.parse('2 + (3 + 4) * 5'))
# print(e.parse('2 + (3 + * 4)'))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "exprparse.py", line 40, in parse
# return self.expr()
# File "exprparse.py", line 67, in expr
# right = self.term()
# File "exprparse.py", line 77, in term
# termval = self.factor()
# File "exprparse.py", line 93, in factor
# exprval = self.expr()
# File "exprparse.py", line 67, in expr
# right = self.term()
# File "exprparse.py", line 77, in term
# termval = self.factor()
# File "exprparse.py", line 97, in factor
# raise SyntaxError("Expected NUMBER or LPAREN")
# SyntaxError: Expected NUMBER or LPAREN
if __name__ == '__main__':
descent_parser()
## 讨论
文本解析是一个很大的主题, 一般会占用学生学习编译课程时刚开始的三周时间。如果你在找寻关于语法,解析算法等相关的背景知识的话,你应该去看一下编译器书籍。很显然,关于这方面的内容太多,不可能在这里全部展开。
尽管如此,编写一个递归下降解析器的整体思路是比较简单的。开始的时候,你先获得所有的语法规则,然后将其转换为一个函数或者方法。因此如果你的语法类似这样:
expr ::= term { ('+'|'-') term }*
term ::= factor { ('*'|'/') factor }*
factor ::= '(' expr ')'
| NUM
你应该首先将它们转换成一组像下面这样的方法:
class ExpressionEvaluator:
...
def expr(self):
...
def term(self):
...
def factor(self):
...
每个方法要完成的任务很简单 - 它必须从左至右遍历语法规则的每一部分,处理每个令牌。从某种意义上讲,方法的目的就是要么处理完语法规则,要么产生一个语法错误。为了这样做,需采用下面的这些实现方法:
- 如果规则中的下个符号是另外一个语法规则的名字(比如term或factor),就简单的调用同名的方法即可。这就是该算法中”下降”的由来 - 控制下降到另一个语法规则中去。有时候规则会调用已经执行的方法(比如,在 `factor ::= '('expr ')'` 中对expr的调用)。这就是算法中”递归”的由来。
- 如果规则中下一个符号是个特殊符号(比如(),你得查找下一个令牌并确认是一个精确匹配)。如果不匹配,就产生一个语法错误。这一节中的 `_expect()` 方法就是用来做这一步的。
- 如果规则中下一个符号为一些可能的选择项(比如 + 或 -),你必须对每一种可能情况检查下一个令牌,只有当它匹配一个的时候才能继续。这也是本节示例中 `_accept()` 方法的目的。它相当于_expect()方法的弱化版本,因为如果一个匹配找到了它会继续,但是如果没找到,它不会产生错误而是回滚(允许后续的检查继续进行)。
- 对于有重复部分的规则(比如在规则表达式 `::= term { ('+'|'-') term }*` 中),重复动作通过一个while循环来实现。循环主体会收集或处理所有的重复元素直到没有其他元素可以找到。
- 一旦整个语法规则处理完成,每个方法会返回某种结果给调用者。这就是在解析过程中值是怎样累加的原理。比如,在表达式求值程序中,返回值代表表达式解析后的部分结果。最后所有值会在最顶层的语法规则方法中合并起来。
尽管向你演示的是一个简单的例子,递归下降解析器可以用来实现非常复杂的解析。比如,Python语言本身就是通过一个递归下降解析器去解释的。如果你对此感兴趣,你可以通过查看Python源码文件Grammar/Grammar来研究下底层语法机制。看完你会发现,通过手动方式去实现一个解析器其实会有很多的局限和不足之处。
其中一个局限就是它们不能被用于包含任何左递归的语法规则中。比如,加入你需要翻译下面这样一个规则:
items ::= items ',' item
| item
为了这样做,你可能会像下面这样使用 `items()` 方法:
def items(self):
itemsval = self.items()
if itemsval and self._accept(','):
itemsval.append(self.item())
else:
itemsval = [ self.item() ]
唯一的问题是这个方法根本不能工作,事实上,它会产生一个无限递归错误。
关于语法规则本身你可能也会碰到一些棘手的问题。比如,你可能想知道下面这个简单扼语法是否表述得当:
expr ::= factor { ('+'|'-'|'*'|'/') factor }*
factor ::= '(' expression ')'
| NUM
这个语法看上去没啥问题,但是它却不能察觉到标准四则运算中的运算符优先级。比如,表达式 `"3 + 4 * 5"` 会得到35而不是期望的23.分开使用”expr”和”term”规则可以让它正确的工作。
对于复杂的语法,你最好是选择某个解析工具比如PyParsing或者是PLY。下面是使用PLY来重写表达式求值程序的代码:
from ply.lex import lex
from ply.yacc import yacc
# Token list
tokens = [ 'NUM', 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN' ]
# Ignored characters
t_ignore = ' \t\n'
# Token specifications (as regexs)
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
# Token processing functions
def t_NUM(t):
r'\d+'
t.value = int(t.value)
return t
# Error handler
def t_error(t):
print('Bad character: {!r}'.format(t.value[0]))
t.skip(1)
# Build the lexer
lexer = lex()
# Grammar rules and handler functions
def p_expr(p):
'''
expr : expr PLUS term
| expr MINUS term
'''
if p[2] == '+':
p[0] = p[1] + p[3]
elif p[2] == '-':
p[0] = p[1] - p[3]
def p_expr_term(p):
'''
expr : term
'''
p[0] = p[1]
def p_term(p):
'''
term : term TIMES factor
| term DIVIDE factor
'''
if p[2] == '*':
p[0] = p[1] * p[3]
elif p[2] == '/':
p[0] = p[1] / p[3]
def p_term_factor(p):
'''
term : factor
'''
p[0] = p[1]
def p_factor(p):
'''
factor : NUM
'''
p[0] = p[1]
def p_factor_group(p):
'''
factor : LPAREN expr RPAREN
'''
p[0] = p[2]
def p_error(p):
print('Syntax error')
parser = yacc()
这个程序中,所有代码都位于一个比较高的层次。你只需要为令牌写正则表达式和规则匹配时的高阶处理函数即可。而实际的运行解析器,接受令牌等等底层动作已经被库函数实现了。
下面是一个怎样使用得到的解析对象的例子:
>>> parser.parse('2')
2
>>> parser.parse('2+3')
5
>>> parser.parse('2+(3+4)*5')
37
>>>
如果你想在你的编程过程中来点挑战和刺激,编写解析器和编译器是个不错的选择。再次,一本编译器的书籍会包含很多底层的理论知识。不过很多好的资源也可以在网上找到。Python自己的ast模块也值得去看一下。
- Copyright
- 前言
- 第一章:数据结构和算法
- 1.1 解压序列赋值给多个变量
- 1.2 解压可迭代对象赋值给多个变量
- 1.3 保留最后N个元素
- 1.4 查找最大或最小的N个元素
- 1.5 实现一个优先级队列
- 1.6 字典中的键映射多个值
- 1.7 字典排序
- 1.8 字典的运算
- 1.9 查找两字典的相同点
- 1.10 删除序列相同元素并保持顺序
- 1.11 命名切片
- 1.12 序列中出现次数最多的元素
- 1.13 通过某个关键字排序一个字典列表
- 1.14 排序不支持原生比较的对象
- 1.15 通过某个字段将记录分组
- 1.16 过滤序列元素
- 1.17 从字典中提取子集
- 1.18 映射名称到序列元素
- 1.19 转换并同时计算数据
- 1.20 合并多个字典或映射
- 第二章:字符串和文本
- 2.1 使用多个界定符分割字符串
- 2.2 字符串开头或结尾匹配
- 2.3 用Shell通配符匹配字符串
- 2.4 字符串匹配和搜索
- 2.5 字符串搜索和替换
- 2.6 字符串忽略大小写的搜索替换
- 2.7 最短匹配模式
- 2.8 多行匹配模式
- 2.9 将Unicode文本标准化
- 2.10 在正则式中使用Unicode
- 2.11 删除字符串中不需要的字符
- 2.12 审查清理文本字符串
- 2.13 字符串对齐
- 2.14 合并拼接字符串
- 2.15 字符串中插入变量
- 2.16 以指定列宽格式化字符串
- 2.17 在字符串中处理html和xml
- 2.18 字符串令牌解析
- 2.19 实现一个简单的递归下降分析器
- 2.20 字节字符串上的字符串操作
- 第三章:数字日期和时间
- 3.1 数字的四舍五入
- 3.2 执行精确的浮点数运算
- 3.3 数字的格式化输出
- 3.4 二八十六进制整数
- 3.5 字节到大整数的打包与解包
- 3.6 复数的数学运算
- 3.7 无穷大与NaN
- 3.8 分数运算
- 3.9 大型数组运算
- 3.10 矩阵与线性代数运算
- 3.11 随机选择
- 3.12 基本的日期与时间转换
- 3.13 计算最后一个周五的日期
- 3.14 计算当前月份的日期范围
- 3.15 字符串转换为日期
- 3.16 结合时区的日期操作
- 第四章:迭代器与生成器
- 4.1 手动遍历迭代器
- 4.2 代理迭代
- 4.3 使用生成器创建新的迭代模式
- 4.4 实现迭代器协议
- 4.5 反向迭代
- 4.6 带有外部状态的生成器函数
- 4.7 迭代器切片
- 4.8 跳过可迭代对象的开始部分
- 4.9 排列组合的迭代
- 4.10 序列上索引值迭代
- 4.11 同时迭代多个序列
- 4.12 不同集合上元素的迭代
- 4.13 创建数据处理管道
- 4.14 展开嵌套的序列
- 4.15 顺序迭代合并后的排序迭代对象
- 4.16 迭代器代替while无限循环
- 第五章:文件与IO
- 5.1 读写文本数据
- 5.2 打印输出至文件中
- 5.3 使用其他分隔符或行终止符打印
- 5.4 读写字节数据
- 5.5 文件不存在才能写入
- 5.6 字符串的I/O操作
- 5.7 读写压缩文件
- 5.8 固定大小记录的文件迭代
- 5.9 读取二进制数据到可变缓冲区中
- 5.10 内存映射的二进制文件
- 5.11 文件路径名的操作
- 5.12 测试文件是否存在
- 5.13 获取文件夹中的文件列表
- 5.14 忽略文件名编码
- 5.15 打印不合法的文件名
- 5.16 增加或改变已打开文件的编码
- 5.17 将字节写入文本文件
- 5.18 将文件描述符包装成文件对象
- 5.19 创建临时文件和文件夹
- 5.20 与串行端口的数据通信
- 5.21 序列化Python对象
- 第六章:数据编码和处理
- 6.1 读写CSV数据
- 6.2 读写JSON数据
- 6.3 解析简单的XML数据
- 6.4 增量式解析大型XML文件
- 6.5 将字典转换为XML
- 6.6 解析和修改XML
- 6.7 利用命名空间解析XML文档
- 6.8 与关系型数据库的交互
- 6.9 编码和解码十六进制数
- 6.10 编码解码Base64数据
- 6.11 读写二进制数组数据
- 6.12 读取嵌套和可变长二进制数据
- 6.13 数据的累加与统计操作
- 第七章:函数
- 7.1 可接受任意数量参数的函数
- 7.2 只接受关键字参数的函数
- 7.3 给函数参数增加元信息
- 7.4 返回多个值的函数
- 7.5 定义有默认参数的函数
- 7.6 定义匿名或内联函数
- 7.7 匿名函数捕获变量值
- 7.8 减少可调用对象的参数个数
- 7.9 将单方法的类转换为函数
- 7.10 带额外状态信息的回调函数
- 7.11 内联回调函数
- 7.12 访问闭包中定义的变量
- 第八章:类与对象
- 8.1 改变对象的字符串显示
- 8.2 自定义字符串的格式化
- 8.3 让对象支持上下文管理协议
- 8.4 创建大量对象时节省内存方法
- 8.5 在类中封装属性名
- 8.6 创建可管理的属性
- 8.7 调用父类方法
- 8.8 子类中扩展property
- 8.9 创建新的类或实例属性
- 8.10 使用延迟计算属性
- 8.11 简化数据结构的初始化
- 8.12 定义接口或者抽象基类
- 8.13 实现数据模型的类型约束
- 8.14 实现自定义容器
- 8.15 属性的代理访问
- 8.16 在类中定义多个构造器
- 8.17 创建不调用init方法的实例
- 8.18 利用Mixins扩展类功能
- 8.19 实现状态对象或者状态机
- 8.20 通过字符串调用对象方法
- 8.21 实现访问者模式
- 8.22 不用递归实现访问者模式
- 8.23 循环引用数据结构的内存管理
- 8.24 让类支持比较操作
- 8.25 创建缓存实例
- 第九章:元编程
- 9.1 在函数上添加包装器
- 9.2 创建装饰器时保留函数元信息
- 9.3 解除一个装饰器
- 9.4 定义一个带参数的装饰器
- 9.5 可自定义属性的装饰器
- 9.6 带可选参数的装饰器
- 9.7 利用装饰器强制函数上的类型检查
- 9.8 将装饰器定义为类的一部分
- 9.9 将装饰器定义为类
- 9.10 为类和静态方法提供装饰器
- 9.11 装饰器为被包装函数增加参数
- 9.12 使用装饰器扩充类的功能
- 9.13 使用元类控制实例的创建
- 9.14 捕获类的属性定义顺序
- 9.15 定义有可选参数的元类
- 9.16 *args和**kwargs的强制参数签名
- 9.17 在类上强制使用编程规约
- 9.18 以编程方式定义类
- 9.19 在定义的时候初始化类的成员
- 9.20 利用函数注解实现方法重载
- 9.21 避免重复的属性方法
- 9.22 定义上下文管理器的简单方法
- 9.23 在局部变量域中执行代码
- 9.24 解析与分析Python源码
- 9.25 拆解Python字节码
- 第十章:模块与包
- 10.1 构建一个模块的层级包
- 10.2 控制模块被全部导入的内容
- 10.3 使用相对路径名导入包中子模块
- 10.4 将模块分割成多个文件
- 10.5 利用命名空间导入目录分散的代码
- 10.6 重新加载模块
- 10.7 运行目录或压缩文件
- 10.8 读取位于包中的数据文件
- 10.9 将文件夹加入到sys.path
- 10.10 通过字符串名导入模块
- 10.11 通过导入钩子远程加载模块
- 10.12 导入模块的同时修改模块
- 10.13 安装私有的包
- 10.14 创建新的Python环境
- 10.15 分发包
- 第十一章:网络与Web编程
- 11.1 作为客户端与HTTP服务交互
- 11.2 创建TCP服务器
- 11.3 创建UDP服务器
- 11.4 通过CIDR地址生成对应的IP地址集
- 11.5 生成一个简单的REST接口
- 11.6 通过XML-RPC实现简单的远程调用
- 11.7 在不同的Python解释器之间交互
- 11.8 实现远程方法调用
- 11.9 简单的客户端认证
- 11.10 在网络服务中加入SSL
- 11.11 进程间传递Socket文件描述符
- 11.12 理解事件驱动的IO
- 11.13 发送与接收大型数组
- 第十二章:并发编程
- 12.1 启动与停止线程
- 12.2 判断线程是否已经启动
- 12.3 线程间的通信
- 12.4 给关键部分加锁
- 12.5 防止死锁的加锁机制
- 12.6 保存线程的状态信息
- 12.7 创建一个线程池
- 12.8 简单的并行编程
- 12.9 Python的全局锁问题
- 12.10 定义一个Actor任务
- 12.11 实现消息发布/订阅模型
- 12.12 使用生成器代替线程
- 12.13 多个线程队列轮询
- 12.14 在Unix系统上面启动守护进程
- 第十三章:脚本编程与系统管理
- 13.1 通过重定向/管道/文件接受输入
- 13.2 终止程序并给出错误信息
- 13.3 解析命令行选项
- 13.4 运行时弹出密码输入提示
- 13.5 获取终端的大小
- 13.6 执行外部命令并获取它的输出
- 13.7 复制或者移动文件和目录
- 13.8 创建和解压压缩文件
- 13.9 通过文件名查找文件
- 13.10 读取配置文件
- 13.11 给简单脚本增加日志功能
- 13.12 给内库增加日志功能
- 13.13 记录程序执行的时间
- 13.14 限制内存和CPU的使用量
- 13.15 启动一个WEB浏览器
- 第十四章:测试调试和异常
- 14.1 测试输出到标准输出上
- 14.2 在单元测试中给对象打补丁
- 14.3 在单元测试中测试异常情况
- 14.4 将测试输出用日志记录到文件中
- 14.5 忽略或者期望测试失败
- 14.6 处理多个异常
- 14.7 捕获所有异常
- 14.8 创建自定义异常
- 14.9 捕获异常后抛出另外的异常
- 14.10 重新抛出最后的异常
- 14.11 输出警告信息
- 14.12 调试基本的程序崩溃错误
- 14.13 给你的程序做基准测试
- 14.14 让你的程序跑的更快
- 第十五章:C语言扩展
- 15.1 使用ctypes访问C代码
- 15.2 简单的C扩展模块
- 15.3 一个操作数组的扩展函数
- 15.4 在C扩展模块中操作隐形指针
- 15.5 从扩张模块中定义和导出C的API
- 15.6 从C语言中调用Python代码
- 15.7 从C扩展中释放全局锁
- 15.8 C和Python中的线程混用
- 15.9 用WSIG包装C代码
- 15.10 用Cython包装C代码
- 15.11 用Cython写高性能的数组操作
- 15.12 将函数指针转换为可调用对象
- 15.13 传递NULL结尾的字符串给C函数库
- 15.14 传递Unicode字符串给C函数库
- 15.15 C字符串转换为Python字符串
- 15.16 不确定编码格式的C字符串
- 15.17 传递文件名给C扩展
- 15.18 传递已打开的文件给C扩展
- 15.19 从C语言中读取类文件对象
- 15.20 处理C语言中的可迭代对象
- 15.21 诊断分析代码错误
- 附录A
- 关于译者
- Roadmap