Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
lmpythw-zh
提交
8fdc1f15
L
lmpythw-zh
项目概览
OpenDocCN
/
lmpythw-zh
大约 1 年 前同步成功
通知
0
Star
18
Fork
5
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
L
lmpythw-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
8fdc1f15
编写于
8月 13, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ex33
上级
450f66f3
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
192 addition
and
7 deletion
+192
-7
ex33.md
ex33.md
+192
-7
未找到文件。
ex33.md
浏览文件 @
8fdc1f15
# 练习 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 代码,再从三个不同的角度讨论解析器:
...
...
@@ -19,7 +27,7 @@ hello(10, 20)
为什么我们这样做?我们需要基于其语法,知道 Python 代码的结构,以便我们稍后分析。如果我们不将记号的线性列表转换成树结构,那么我们不知道函数,代码块,分支或表达式的边界在哪里。我们必须以“直线”方式在飞行中确定边界,这不容易使其可靠。很多早期的糟糕语言是直线语言,我们现在知道了他们不必须是这样。我们可以使用解析器构建树结构。
解析器的任务是从扫描器中获取记号列表,并将其翻译成更有意义的语法树。
您
可以认为解析器是,对记号流应用另一个正则表达式。扫描器的正则表达式将大量字符放入记号中。解析器的“正则表达式”将这些记号放在盒子里面,它里面有盒子,以此类推,直到记号不再是线性的。
解析器的任务是从扫描器中获取记号列表,并将其翻译成更有意义的语法树。
你
可以认为解析器是,对记号流应用另一个正则表达式。扫描器的正则表达式将大量字符放入记号中。解析器的“正则表达式”将这些记号放在盒子里面,它里面有盒子,以此类推,直到记号不再是线性的。
解析器也为这些盒子添加了含义。解析器将简单地删除
`()`
括号记号,并为可能的
`Function`
类创建一个特殊的
`parameters`
列表。它会删除冒号,无用的空格,逗号,任何没有真正意义的记号,并将其转换为更易于处理的嵌套结构。最后的结果可能看起来像,上面的示例代码的伪造树:
...
...
@@ -43,11 +51,11 @@ hello(10, 20)
## 递归下降解析
有几种已建立的方法,可以为这种语法创建解析器,但最简单的方法称为递归下降解析器(RDP)。我实际上在我《笨办法学 Python》练习 49 中讲解了这个话题。
您创建了一个简单的 RDP 解析器来处理您的小游戏语言,您甚至不了解它。在本练习中,我将对如何编写 RDP 解析器进行更正式的描述,然后让您
使用我们上面的 Python 小代码片段来尝试它。
有几种已建立的方法,可以为这种语法创建解析器,但最简单的方法称为递归下降解析器(RDP)。我实际上在我《笨办法学 Python》练习 49 中讲解了这个话题。
你创建了一个简单的 RDP 解析器来处理你的小游戏语言,你甚至不了解它。在本练习中,我将对如何编写 RDP 解析器进行更正式的描述,然后让你
使用我们上面的 Python 小代码片段来尝试它。
RDP 使用多个相互递归的函数调用,它实现了给定语法的树形结构。RDP 解析器的代码看起来像
您正在处理的实际语法,只要遵循一些规则,它们就很容易编写。RDP 解析器的两个缺点是:它们可能不是非常有效,并且通常需要手动编写它们,因此它们的错误比生成的解析器更多。对于 RDP 解析器可以解析的东西,还有一些理论上的限制,但是由于您手动编写它们,您
通常可以解决很多限制。
RDP 使用多个相互递归的函数调用,它实现了给定语法的树形结构。RDP 解析器的代码看起来像
你正在处理的实际语法,只要遵循一些规则,它们就很容易编写。RDP 解析器的两个缺点是:它们可能不是非常有效,并且通常需要手动编写它们,因此它们的错误比生成的解析器更多。对于 RDP 解析器可以解析的东西,还有一些理论上的限制,但是由于你手动编写它们,你
通常可以解决很多限制。
为了编写一个 RDP 解析器,
您
需要使用三个主要操作,来处理扫描器的记号:
为了编写一个 RDP 解析器,
你
需要使用三个主要操作,来处理扫描器的记号:
> `peek`
...
...
@@ -63,7 +71,7 @@ RDP 使用多个相互递归的函数调用,它实现了给定语法的树形
你会注意到,这些是我在练习 33 中让你为扫描器创建的三个操作,这就是为什么。你需要他们来实现一个 RDP 解析器。
您可以使用这三个函数来编写语法解析函数,从扫描仪
中获取记号。这个练习的一个简短的例子是,解析这个简单的函数:
你可以使用这三个函数来编写语法解析函数,从扫描器
中获取记号。这个练习的一个简短的例子是,解析这个简单的函数:
```
py
def
function_definition
(
tokens
):
...
...
@@ -76,5 +84,182 @@ def function_definition(tokens):
return
{
'type'
:
'FUNCDEF'
,
'name'
:
name
,
'params'
:
params
}
```
你可以看到我只是接受记号并使用
`match`
和
`skip`
处理它们。您还会注意到我有一个
`parameters`
函数,它是“递归下降解析器”的“递归”部分。当它需要为函数解析参数时,
`function_definition`
会调用
`parameters`
。
你可以看到我只是接受记号并使用
`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`
用法更好。
你应该实现
`PunyPythonPython`
,它可以解析这个微小的 Python 语言,并执行以下操作:
+
不是仅仅产生
`dicts`
的列表,你应该为每个语法生产式的结果创建类。这些类之后成为列表中的对象。
+
这些类只需要存储被解析的记号,但是要准备做更多事情。
+
你只需要解析这个微小的语言,但你应该尝试解决“Python 缩进”问题。你可能需要秀阿贵扫描器,使其更智能,才能在行的开头匹配
`INDENT`
空白字符,并在其他位置忽略它。你还需要跟踪如何多少缩进了多少,同时也记录零缩进,所以你可以“压缩”代码块。
一个泛用的测试套件涉及到,将这个微小的 python 的更多样本交给解析器,但现在只需要得到一个小文件来解析。尝试在测试中获得良好的覆盖率,并尽可能多地发现错误。
## 研究性学习
这个练习相当庞大,所以只需要完成。花点时间,一次做一点点。我强烈建议学习我这里的小型样本,直到你完全弄清楚,并打印正在处理的关键位置的记号。
## 深入学习
查看 David Beazley 的
[
SLY 解析器生成器
](
http://sly.readthedocs.io/en/latest/
)
,以便让你的计算机为你生成你的解析器和扫描器(也称为分词器)。随意尝试用 SLY 重复此练习来进行比较。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录