💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 练习 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,然后将它们组合来产生大段代码,可以可靠地匹配所有记号。这样做的优点是,你可以以滚动方式为这些生成的扫描器提供独立的字符,并使其快速识别记号。它比我这里的方式要好,其中我拼凑字符串,并尝试一系列正则表达式,直到找到一个正则表达式。 研究扫描器的发生器如何工作,并将其与你编写的代码进行比较。