Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
lmpythw-zh
提交
4ee1c0ec
L
lmpythw-zh
项目概览
OpenDocCN
/
lmpythw-zh
9 个月 前同步成功
通知
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,体验更适合开发者的 AI 搜索 >>
提交
4ee1c0ec
编写于
8月 12, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ex32
上级
94467e15
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
162 addition
and
0 deletion
+162
-0
ex32.md
ex32.md
+162
-0
未找到文件。
ex32.md
0 → 100644
浏览文件 @
4ee1c0ec
# 练习 32:扫描器
> 原文:[Exercise 32: Scanners](https://learncodethehardway.org/more-python-book/ex32.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
我的第一本书在练习 48 中非常偶然涉及到了扫描器,但现在我们将会更加正式。我将解释扫描文本背后的概念,它与正则表达式有关,以及如何为一小段 Python 代码创建一个小型扫描器。
我们以下面的 Python 代码为例来开始讨论:
```
py
def
hello
(
x
,
y
):
print
(
x
+
y
)
hello
(
10
,
20
)
```
你已经在 Python 上练习了一段时间了,所以你的大脑最有可能很快阅读这个代码,但是你真的明白了吗?当我(或别人)教你 Python 时,我让你记得所有的“符号”。
`def`
和
`()`
字符是每一个符号,但是 Python 需要一种可靠的、一致的方法来处理它们。Python 还需要能够读取
`hello`
,理解它是一个什么东西的“名称”,然后知道
`def hello(x, y)`
和
`hello(10, 20)`
之间的区别。怎么实现它呢?
执行此操作的第一步是,扫描文本并查找“记号”(Token)。在扫描阶段,像 Python 这样的语言不会首先关心什么是符号(
`def`
),什么是名称(
`hello`
)。它将简单地,尝试将输入语言转换为的文本模式串,成为“记号”。它通过应用一系列正则表达式来做到这一点,这些正则表达式“匹配” Python 理解的每个可能的输入。练习 31 中,你会记得一个正则表达式是一种方式,告诉 Python 要匹配或接受什么字符序列。所有 Python 解释器都使用许多正则表达式,来匹配它理解的每个记号。
如果你看看上面的代码,你可以编写一组正则表达式来处理它。
`def`
需要一个简单的正则表达式,只是“def”。对于
`()+:,`
字符你需要更多的正则表达式。然后,你还剩下如何处理
`print`
,
`hello`
,
`10`
和
`20`
。
一旦你确定了上述代码示例中的所有符号,你需要命名它们。你不能仅仅通过它们的正则表达式来引用它们,因为查找效率低下,也令人困惑。稍后你会发现,为每个符号提供自己的名字(或数字)可以简化解析,但现在让我们为这些正则表达式设计一些名称。我可以说
`def`
只是
`DEF`
,那么
`()+:,`
可以是
`LPAREN RPAREN PLUS COLON COMMA`
。之后,我可以将用于
`hello `
和
`print `
之类的单词正则表达式称为
`NAME`
。通过这样做,我想出了一种方法,将原始文本流转换成一个单个数字(或名称)记号的流,来在后期使用。
Python 也很棘手,因为它需要一个前导空白的正则表达式,来处理代码块的缩进和压缩。现在,让我们使用一个相当笨的
`^\s+`
,然后假装它也捕捉到行的开头使用了多少个空白。
最终你会拥有一组正则表达式,可以处理上面的代码,它可能看起来像这样:
| 正则表达式 | 记号 |
| --- | --- |
|
`def`
|
`DEF`
|
|
`[a-zA-Z_][a-zA-Z0-9_]*`
|
`NAME`
|
|
`[0-9]+`
|
`INTEGER`
|
|
`\(`
|
`LPAREN`
|
|
`\)`
|
`RPAREN`
|
|
`\+`
|
`PLUS`
|
|
`:`
|
`COLON`
|
|
`,`
|
`COMMA`
|
|
`^\s+`
|
`INDENT`
|
扫描器的任务是使用这些正则表达式,并将输入文本分解成识别符号的流。如果我这样对示例代码这么做,我可以产生:
```
DEF NAME(hello) LPAREN NAME(x) COMMA NAME(y) RPAREN COLON
INDENT(4) NAME(print) LPAREN NAME(x) PLUS NAME(y) RPAREN
NAME(hello) RPAREN INTEGER(10) COMMA INTEGER(20) RPAREN
```
研究此转换,匹配扫描器输出的每一行,并使用表中的正则表达式将其与上述 Python 代码进行比较。你会看到这只是选取输入文本,将每个正则表达式匹配到记录名称,然后保存所需的任何信息,如
`hello`
或数字
`10`
。
## 微小的 Python 扫描器
我编写了一个非常小的 Python 扫描器,演示了这个非常小的 Python 语言:
```
py
import
re
code
=
[
"def hello(x, y):"
,
" print(x + y)"
,
"hello(10, 20)"
,
]
TOKENS
=
[
(
re
.
compile
(
r
"^def"
),
"DEF"
),
(
re
.
compile
(
r
"^[a-zA-Z_][a-zA-Z0-9_]*"
),
"NAME"
),
(
re
.
compile
(
r
"^[0-9]+"
),
"INTEGER"
),
(
re
.
compile
(
r
"^\("
),
"LPAREN"
),
(
re
.
compile
(
r
"^\)"
),
"RPAREN"
),
(
re
.
compile
(
r
"^\+"
),
"PLUS"
),
(
re
.
compile
(
r
"^:"
),
"COLON"
),
(
re
.
compile
(
r
"^,"
),
"COMMA"
),
(
re
.
compile
(
r
"^\s+"
),
"INDENT"
),
]
def
match
(
i
,
line
):
start
=
line
[
i
:]
for
regex
,
token
in
TOKENS
:
match
=
regex
.
match
(
start
)
if
match
:
begin
,
end
=
match
.
span
()
return
token
,
start
[:
end
],
end
return
None
,
start
,
None
script
=
[]
for
line
in
code
:
i
=
0
while
i
<
len
(
line
):
token
,
string
,
end
=
match
(
i
,
line
)
assert
token
,
"Failed to match line %s"
%
string
if
token
:
i
+=
end
script
.
append
((
token
,
string
,
i
,
end
))
print
(
script
)
```
当你运行这个脚本时,你会得到一个
`tuples`
的
`list`
,它是
`TOKEN`
、匹配到的字符串、开头和末尾,像这样:
```
py
[(
'DEF'
,
'def'
,
3
,
3
),
(
'INDENT'
,
' '
,
4
,
1
),
(
'NAME'
,
'hello'
,
9
,
5
),
(
'LPAREN'
,
'('
,
10
,
1
),
(
'NAME'
,
'x'
,
11
,
1
),
(
'COMMA'
,
','
,
12
,
1
),
(
'INDENT'
,
' '
,
13
,
1
),
(
'NAME'
,
'y'
,
14
,
1
),
(
'RPAREN'
,
')'
,
15
,
1
),
(
'COLON'
,
':'
,
16
,
1
),
(
'INDENT'
,
' '
,
4
,
4
),
(
'NAME'
,
'print'
,
9
,
5
),
(
'LPAREN'
,
'('
,
10
,
1
),
(
'NAME'
,
'x'
,
11
,
1
),
(
'INDENT'
,
' '
,
12
,
1
),
(
'PLUS'
,
'+'
,
13
,
1
),
(
'INDENT'
,
' '
,
14
,
1
),
(
'NAME'
,
'y'
,
15
,
1
),
(
'RPAREN'
,
')'
,
16
,
1
),
(
'NAME'
,
'hello'
,
5
,
5
),
(
'LPAREN'
,
'('
,
6
,
1
),
(
'INTEGER'
,
'10'
,
8
,
2
),
(
'COMMA'
,
','
,
9
,
1
),
(
'INDENT'
,
' '
,
10
,
1
),
(
'INTEGER'
,
'20'
,
12
,
2
),
(
'RPAREN'
,
')'
,
13
,
1
)]
```
这个代码绝对不是你可以创建的最快或最准确的扫描器。这是一个简单的脚本,用于演示扫描器的工作原理。对于进行真正的扫描工作,你将使用一种工具来生成更高效的扫描器。我在深入学习部分介绍。
## 挑战练习
你的工作是研究这个扫描器示例代码,并将其转换成通用的
`Scanner`
类以便稍后使用。这个
`Scanner`
类的目标是接受一个输入文件,将其扫描为记号的列表,然后允许你按顺序取出记号。API 应具有以下功能:
> `__init__`
> 使用类似的元组列表(没有`re.compile`)来配置扫描器。
> `scan`
> 接受一个字符串并执行扫描,创建一个记录列表以便以后使用。你应该保留这个字符串,让人们以后访问。
> `match`
> 提供可能的记号列表,返回列表中的第一个记号,并将其移除。
> `peek`
> 提供可能的记号列表,返回列表中的第一个记号,但不将其移除。
> `push`
> 将记号放回记号流中,以便后续的`peek`或者`match`返回它。
你也应该创建通用的
`Token`
类来代替我使用的
`tuple`
。它应该能够跟踪发现的记号,匹配的字符串、原始字符串中匹配位置的开头和末尾。
## 研究性学习
+
安装
`pytest-cov`
库,并使用它来测量自动化测试的覆盖率。
+
使用
`pytest-cov`
的结果来改进自动化测试。
## 深入学习
创建扫描器的更好方法是,利用以下关于正则表达式的三个事实:
+
正则表达式是有限状态机。
+
你可以将小型有限状态机精确地组合成更大更复杂的有限状态机。
+
匹配许多小型正则表达式的有限状态机组合,操作方式每个正则表达式一样,并且效率更高。
有许多工具使用这个事实来接受扫描器定义,将每个小的正则表达式转换为 FSM,然后将它们组合来产生大段代码,可以可靠地匹配所有记号。这样做的优点是,你可以以滚动方式为这些生成的扫描器提供独立的字符,并使其快速识别记号。它比我这里的方式要好,其中我拼凑字符串,并尝试一系列正则表达式,直到找到一个正则表达式。
研究扫描器的发生器如何工作,并将其与你编写的代码进行比较。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录