ex33.md 13.7 KB
Newer Older
W
ex33.  
wizardforcel 已提交
1 2
# 练习 33:解析器

W
ex33  
wizardforcel 已提交
3 4 5 6 7 8 9 10 11
> 原文:[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/)

想象一下,你将获得一个巨大的数字列表,你必须将其输入到电子表格中。一开始,这个巨大的列表只是一个空格分隔的原始数据流。你的大脑会自动在空格处拆分数字流并创建数字。你的大脑像扫描器一样。然后,你将获取每个数字,并将其输入到具有含义的行和列中。你的大脑像一个解析器,通过获取扁平的数字(记号),并将它们变成一个更有意义的行和列的二维网格。你遵循的规则,什么数字进入什么行什么列,是你的“语法”,解析器的工作就是像你对于电子表格那样使用语法。
W
ex33.  
wizardforcel 已提交
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

我们再来看一下练习 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 代码的结构,以便我们稍后分析。如果我们不将记号的线性列表转换成树结构,那么我们不知道函数,代码块,分支或表达式的边界在哪里。我们必须以“直线”方式在飞行中确定边界,这不容易使其可靠。很多早期的糟糕语言是直线语言,我们现在知道了他们不必须是这样。我们可以使用解析器构建树结构。

W
ex33  
wizardforcel 已提交
30
解析器的任务是从扫描器中获取记号列表,并将其翻译成更有意义的语法树。你可以认为解析器是,对记号流应用另一个正则表达式。扫描器的正则表达式将大量字符放入记号中。解析器的“正则表达式”将这些记号放在盒子里面,它里面有盒子,以此类推,直到记号不再是线性的。
W
ex33.  
wizardforcel 已提交
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

解析器也为这些盒子添加了含义。解析器将简单地删除`()`括号记号,并为可能的`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
```

## 递归下降解析

W
ex33  
wizardforcel 已提交
54
有几种已建立的方法,可以为这种语法创建解析器,但最简单的方法称为递归下降解析器(RDP)。我实际上在我《笨办法学 Python》练习 49 中讲解了这个话题。你创建了一个简单的 RDP 解析器来处理你的小游戏语言,你甚至不了解它。在本练习中,我将对如何编写 RDP 解析器进行更正式的描述,然后让你使用我们上面的 Python 小代码片段来尝试它。
W
ex33.  
wizardforcel 已提交
55

W
ex33  
wizardforcel 已提交
56
RDP 使用多个相互递归的函数调用,它实现了给定语法的树形结构。RDP 解析器的代码看起来像你正在处理的实际语法,只要遵循一些规则,它们就很容易编写。RDP 解析器的两个缺点是:它们可能不是非常有效,并且通常需要手动编写它们,因此它们的错误比生成的解析器更多。对于 RDP 解析器可以解析的东西,还有一些理论上的限制,但是由于你手动编写它们,你通常可以解决很多限制。
W
ex33.  
wizardforcel 已提交
57

W
ex33  
wizardforcel 已提交
58
为了编写一个 RDP 解析器,你需要使用三个主要操作,来处理扫描器的记号:
W
ex33.  
wizardforcel 已提交
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73

> `peek`

> 如果下一个记号能够匹配,返回它,但是不从流中移除。

> `match`

> 匹配下一个记号,并且从流中移除。

> `skip`

> 由于不需要下个记号,跳过它,将其从流中移除。

你会注意到,这些是我在练习 33 中让你为扫描器创建的三个操作,这就是为什么。你需要他们来实现一个 RDP 解析器。

W
ex33  
wizardforcel 已提交
74
你可以使用这三个函数来编写语法解析函数,从扫描器中获取记号。这个练习的一个简短的例子是,解析这个简单的函数:
W
ex33.  
wizardforcel 已提交
75 76 77 78 79 80 81 82 83 84 85 86

```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}
```

W
ex33  
wizardforcel 已提交
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
你可以看到我只是接受记号并使用`match``skip`处理它们。你还会注意到我有一个`parameters`函数,它是“递归下降解析器”的“递归”部分。当它需要为函数解析参数时,`function_definition`会调用`parameters`

## BNF 语法

尝试从头开始编写一个 RDP 解析器是没有某种形式的语法规范的,有点棘手。你还记得当我要求你将单个正则表达式转换成 FSM 吗?这很难吗?它需要更多的代码,不只是正则表达式中的几个字符。当你为这个练习编写 RDP 解析器时,你将会做类似的事情,因此它有助于使用一种语言,它是“语法的正则表达式”。

最常见的“语法的正则表达式”被称为 Backus–Naur Form(BNF),以创作者 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`用法更好。

W
fix  
wizardforcel 已提交
250
你应该实现`PunyPythonPython`,它可以解析这个微型 Python 语言,并执行以下操作:
W
ex33  
wizardforcel 已提交
251 252 253

+   不是仅仅产生`dicts`的列表,你应该为每个语法生产式的结果创建类。这些类之后成为列表中的对象。
+   这些类只需要存储被解析的记号,但是要准备做更多事情。
W
fix  
wizardforcel 已提交
254
+   你只需要解析这个微型语言,但你应该尝试解决“Python 缩进”问题。你可能需要秀阿贵扫描器,使其更智能,才能在行的开头匹配`INDENT`空白字符,并在其他位置忽略它。你还需要跟踪如何多少缩进了多少,同时也记录零缩进,所以你可以“压缩”代码块。
W
ex33  
wizardforcel 已提交
255 256


W
fix  
wizardforcel 已提交
257
一个泛用的测试套件涉及到,将这个微型 python 的更多样本交给解析器,但现在只需要得到一个小文件来解析。尝试在测试中获得良好的覆盖率,并尽可能多地发现错误。
W
ex33  
wizardforcel 已提交
258 259 260 261 262 263

## 研究性学习

这个练习相当庞大,所以只需要完成。花点时间,一次做一点点。我强烈建议学习我这里的小型样本,直到你完全弄清楚,并打印正在处理的关键位置的记号。

## 深入学习
W
ex33.  
wizardforcel 已提交
264

W
ex33  
wizardforcel 已提交
265
查看 David Beazley 的 [SLY 解析器生成器](http://sly.readthedocs.io/en/latest/),以便让你的计算机为你生成你的解析器和扫描器(也称为分词器)。随意尝试用 SLY 重复此练习来进行比较。