1
0
mirror of https://github.com/apachecn/lmpythw-zh.git synced 2025-05-28 12:02:19 +00:00
lmpythw-zh/ex33.md
wizardforcel f100c6bd82 fix
2017-08-13 15:28:06 +08:00

266 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 练习 33解析器
> 原文:[Exercise 33: Parsers](https://learncodethehardway.org/more-python-book/ex33.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
想象一下,你将获得一个巨大的数字列表,你必须将其输入到电子表格中。一开始,这个巨大的列表只是一个空格分隔的原始数据流。你的大脑会自动在空格处拆分数字流并创建数字。你的大脑像扫描器一样。然后,你将获取每个数字,并将其输入到具有含义的行和列中。你的大脑像一个解析器,通过获取扁平的数字(记号),并将它们变成一个更有意义的行和列的二维网格。你遵循的规则,什么数字进入什么行什么列,是你的“语法”,解析器的工作就是像你对于电子表格那样使用语法。
我们再来看一下练习 32 中的微型 Python 代码,再从三个不同的角度讨论解析器:
```py
def hello(x, y):
print(x + y)
hello(10, 20)
```
当你查看这个代码时,你看到什么?我看到一棵树,类似于我们之前创建的`BSTree``TSTree`。你看到树了吗?我们从这个文件的最上方开始,学习如何将字符转换为树。
首先,当我们加载一个`.py`文件时,它只是一个“字符”流 - 实际上是字节,但 Python 使用Unicode所以必须处理字符。这些字符在一行中毫无结构扫描器的任务是增加第一层次的意义。扫描器通过使用正则表达式从字符串流中提取意义创建记号列表。我们已经将一个字符列表转换为一个记号列表但看看`def hello(x,y):`函数。这是一个函数,里面有代码块。这意味着某种形式的“包含”或“东西里面的东西”的结构。
一个很容易表示包含的方式是用一棵树。我们可以使用表格,像你的电子表格一样,但它并不像树那么容易。接下来看看`hello(x, y)`部分。我们有一个`NAME(hello)`记号,但是我们要抓取`(...)`部分的内容,并且知道它在括号内。再次,我们可以使用一个树,我们将`(...)`部分中的`x, y`部分“嵌套” 为树的子节点或分支。最终,我们就拥有了一棵树,从这个 Python 代码的根开始,并且每个代码块,`print`,函数定义和函数调用都是根的分支,它们也有子分支,以此类推。
为什么我们这样做?我们需要基于其语法,知道 Python 代码的结构,以便我们稍后分析。如果我们不将记号的线性列表转换成树结构,那么我们不知道函数,代码块,分支或表达式的边界在哪里。我们必须以“直线”方式在飞行中确定边界,这不容易使其可靠。很多早期的糟糕语言是直线语言,我们现在知道了他们不必须是这样。我们可以使用解析器构建树结构。
解析器的任务是从扫描器中获取记号列表,并将其翻译成更有意义的语法树。你可以认为解析器是,对记号流应用另一个正则表达式。扫描器的正则表达式将大量字符放入记号中。解析器的“正则表达式”将这些记号放在盒子里面,它里面有盒子,以此类推,直到记号不再是线性的。
解析器也为这些盒子添加了含义。解析器将简单地删除`()`括号记号,并为可能的`Function`类创建一个特殊的`parameters`列表。它会删除冒号,无用的空格,逗号,任何没有真正意义的记号,并将其转换为更易于处理的嵌套结构。最后的结果可能看起来像,上面的示例代码的伪造树:
```
* root
* Function
- name = hello
- parameters = x, y
- code:
* Call
- name = print
- parameters =
* Expression
- Add
- a = x
- b = y
* Call
- name = hello
- parameters = 10, 20
```
## 递归下降解析
有几种已建立的方法可以为这种语法创建解析器但最简单的方法称为递归下降解析器RDP。我实际上在我《笨办法学 Python》练习 49 中讲解了这个话题。你创建了一个简单的 RDP 解析器来处理你的小游戏语言,你甚至不了解它。在本练习中,我将对如何编写 RDP 解析器进行更正式的描述,然后让你使用我们上面的 Python 小代码片段来尝试它。
RDP 使用多个相互递归的函数调用它实现了给定语法的树形结构。RDP 解析器的代码看起来像你正在处理的实际语法只要遵循一些规则它们就很容易编写。RDP 解析器的两个缺点是:它们可能不是非常有效,并且通常需要手动编写它们,因此它们的错误比生成的解析器更多。对于 RDP 解析器可以解析的东西,还有一些理论上的限制,但是由于你手动编写它们,你通常可以解决很多限制。
为了编写一个 RDP 解析器,你需要使用三个主要操作,来处理扫描器的记号:
> `peek`
> 如果下一个记号能够匹配,返回它,但是不从流中移除。
> `match`
> 匹配下一个记号,并且从流中移除。
> `skip`
> 由于不需要下个记号,跳过它,将其从流中移除。
你会注意到,这些是我在练习 33 中让你为扫描器创建的三个操作,这就是为什么。你需要他们来实现一个 RDP 解析器。
你可以使用这三个函数来编写语法解析函数,从扫描器中获取记号。这个练习的一个简短的例子是,解析这个简单的函数:
```py
def function_definition(tokens):
skip(tokens) # discard def
name = match(tokens, 'NAME')
match(tokens, 'LPAREN')
params = parameters(tokens)
match(tokens, 'RPAREN')
match(tokens, 'COLON')
return {'type': 'FUNCDEF', 'name': name, 'params': params}
```
你可以看到我只是接受记号并使用`match``skip`处理它们。你还会注意到我有一个`parameters`函数,它是“递归下降解析器”的“递归”部分。当它需要为函数解析参数时,`function_definition`会调用`parameters`
## BNF 语法
尝试从头开始编写一个 RDP 解析器是没有某种形式的语法规范的,有点棘手。你还记得当我要求你将单个正则表达式转换成 FSM 吗?这很难吗?它需要更多的代码,不只是正则表达式中的几个字符。当你为这个练习编写 RDP 解析器时,你将会做类似的事情,因此它有助于使用一种语言,它是“语法的正则表达式”。
最常见的“语法的正则表达式”被称为 BackusNaur FormBNF以创作者 John Backus 和 Peter Naur 命名。BNF 描述了所需的记号以及这些记号如何重复来形成语言的语法。BNF 还使用与正则表达式相同的符号,所以`*``+``?`有相似的含义。
对于这个练习,我将使用 <https://tools.ietf.org/html/rfc5234> 上面的 IETF 增强 BNF 语法,来规定上面的微型 Python 代码段的语法。ABNF 运算符大部分与正则表达式相同,只是由于某种奇怪的原因,它们在要重复的东西之前放置重复符号。除此之外,请阅读规范,并尝试弄清楚下面的意思:
```
root = *(funccal / funcdef)
funcdef = DEF name LPAREN params RPAREN COLON body
funccall = name LPAREN params RPAREN
params = expression *(COMMA expression)
expression = name / plus / integer
plus = expression PLUS expression
PLUS = "+"
LPAREN = "("
RPAREN = ")"
COLON = ":"
COMMA = ","
DEF = "def"
```
让我们仅仅查看`funcdef `那一行,并将其与`function_definition` Python 代码比较,匹配每一个部分:
`funcdef =`
我们使用`def function_definition(tokens)`来复制,并且它是我们的语法的这个部分的开始。
`DEF`
它在语法中规定了`DEF = "def"`,并且在 Python 代码中,我们使用`skip(tokens)`跳过了它。
`name`
我需要它,所以我使用`name = match(tokens, 'NAME')`匹配它。我使用 CAPITALS 的约定,在 BNF 中表示我会跳过的东西。
`LPAREN`
我假设我收到了一个`def`,但是现在我打算确保有一个`(`,所以我要匹配它。但是我使用`match(tokens, 'LPAREN')`来忽略结果。它就像“需要但是忽略”。
`params`
在 BNF 中我将`params`定义为了新的“语法产生式”,或者“语法规则”。意思是在我的 Python 代码中,我需要一个新的函数。这个函数中,我可以使用`params = parameters(tokens)`来调用那个函数。之后我定义了`parameters`函数来为函数处理逗号分隔的参数。
`RPAREN`
同样我需要但是去掉了它,使用`match(tokens, 'RPAREN')`
`COLON`
同样,我去掉了匹配`match(tokens, 'COLON')`
`body`
我这里实际上跳过了函数体,因为 Python 的缩进语法对于这个例子太难了。你不需要在练习中处理这个例子,除非你喜欢它。
这基本上是,你如何读取 ABNF 规范,并将其系统地转换为代码。你从根开始,将每个语法产生式实现为一个函数,并让扫描器处理简单的记号(我用`CAPITAL`(大写)字母表示)。
## 简单的示例黑魔法解析器
这是我快速 Hack 出来的 RDP 解析器,你可以使用它,作为你的更正式和简洁的解析器的基础。
```py
from scanner import *
from pprint import pprint
def root(tokens):
"""root = *(funccal / funcdef)"""
first = peek(tokens)
if first == 'DEF':
return function_definition(tokens)
elif first == 'NAME':
name = match(tokens, 'NAME')
second = peek(tokens)
if second == 'LPAREN':
return function_call(tokens, name)
else:
assert False, "Not a FUNCDEF or FUNCCALL"
def function_definition(tokens):
"""
funcdef = DEF name LPAREN params RPAREN COLON body
I ignore body for this example 'cause that's hard.
I mean, so you can learn how to do it.
"""
skip(tokens) # discard def
name = match(tokens, 'NAME')
match(tokens, 'LPAREN')
params = parameters(tokens)
match(tokens, 'RPAREN')
match(tokens, 'COLON')
return {'type': 'FUNCDEF', 'name': name, 'params': params}
def parameters(tokens):
"""params = expression *(COMMA expression)"""
params = []
start = peek(tokens)
while start != 'RPAREN':
params.append(expression(tokens))
start = peek(tokens)
if start != 'RPAREN':
assert match(tokens, 'COMMA')
return params
def function_call(tokens, name):
"""funccall = name LPAREN params RPAREN"""
match(tokens, 'LPAREN')
params = parameters(tokens)
match(tokens, 'RPAREN')
return {'type': 'FUNCCALL', 'name': name, 'params': params}
def expression(tokens):
"""expression = name / plus / integer"""
start = peek(tokens)
if start == 'NAME':
name = match(tokens, 'NAME')
if peek(tokens) == 'PLUS':
return plus(tokens, name)
else:
return name
elif start == 'INTEGER':
number = match(tokens, 'INTEGER')
if peek(tokens) == 'PLUS':
return plus(tokens, number)
else:
return number
else:
assert False, "Syntax error %r" % start
def plus(tokens, left):
"""plus = expression PLUS expression"""
match(tokens, 'PLUS')
right = expression(tokens)
return {'type': 'PLUS', 'left': left, 'right': right}
def main(tokens):
results = []
while tokens:
results.append(root(tokens))
return results
parsed = main(scan(code))
pprint(parsed)
```
你会注意到,我正在使用我写的`scanner`模块,拥有我的`match``peek``skip``scan`函数。我使用`from scanner import *`,仅使这个例子更容易理解。你应该使用你的`Scanner`类。
你会注意到,我把这个小解析器的 ABNF 放在每个函数的文档注释中。这有助于我编写每个解析器代码,稍后可以用于错误报告。在尝试挑战练习之前,你应该研究此解析器,甚至可能作为“代码大师副本”。
## 挑战练习
你的下一个挑战是,将你的 `Scanner`类与新编写的`Parser`类组合在一起,你可以派生并重新实现使我这里的简单的解析器。你的基础`Parser`类应该能够:
+ 接受一个`Scanner`对象并执行自身。你可以假设任何默认函数是语法的起始。
+ 拥有错误处理代码,比我简单的`assert`用法更好。
你应该实现`PunyPythonPython`,它可以解析这个微型 Python 语言,并执行以下操作:
+ 不是仅仅产生`dicts`的列表,你应该为每个语法生产式的结果创建类。这些类之后成为列表中的对象。
+ 这些类只需要存储被解析的记号,但是要准备做更多事情。
+ 你只需要解析这个微型语言但你应该尝试解决“Python 缩进”问题。你可能需要秀阿贵扫描器,使其更智能,才能在行的开头匹配`INDENT`空白字符,并在其他位置忽略它。你还需要跟踪如何多少缩进了多少,同时也记录零缩进,所以你可以“压缩”代码块。
一个泛用的测试套件涉及到,将这个微型 python 的更多样本交给解析器,但现在只需要得到一个小文件来解析。尝试在测试中获得良好的覆盖率,并尽可能多地发现错误。
## 研究性学习
这个练习相当庞大,所以只需要完成。花点时间,一次做一点点。我强烈建议学习我这里的小型样本,直到你完全弄清楚,并打印正在处理的关键位置的记号。
## 深入学习
查看 David Beazley 的 [SLY 解析器生成器](http://sly.readthedocs.io/en/latest/),以便让你的计算机为你生成你的解析器和扫描器(也称为分词器)。随意尝试用 SLY 重复此练习来进行比较。