您的位置:首页 > 编程语言 > Python开发

笨办法学 Python · 续 练习 32:扫描器

2017-08-12 20:14 260 查看

练习 32:扫描器

原文:Exercise 32: Scanners

译者:飞龙

协议:CC BY-NC-SA 4.0

自豪地采用谷歌翻译

我的第一本书在练习 48 中非常偶然涉及到了扫描器,但现在我们将会更加正式。我将解释扫描文本背后的概念,它与正则表达式有关,以及如何为一小段 Python 代码创建一个小型扫描器。

我们以下面的 Python 代码为例来开始讨论:

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 语言:

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
、匹配到的字符串、开头和末尾,像这样:

[('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,然后将它们组合来产生大段代码,可以可靠地匹配所有记号。这样做的优点是,你可以以滚动方式为这些生成的扫描器提供独立的字符,并使其快速识别记号。它比我这里的方式要好,其中我拼凑字符串,并尝试一系列正则表达式,直到找到一个正则表达式。

研究扫描器的发生器如何工作,并将其与你编写的代码进行比较。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: