# 練習 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,然后將它們組合來產生大段代碼,可以可靠地匹配所有記號。這樣做的優點是,你可以以滾動方式為這些生成的掃描器提供獨立的字符,并使其快速識別記號。它比我這里的方式要好,其中我拼湊字符串,并嘗試一系列正則表達式,直到找到一個正則表達式。
研究掃描器的發生器如何工作,并將其與你編寫的代碼進行比較。
- 笨辦法學 Python · 續 中文版
- 引言
- 第一部分:預備知識
- 練習 0:起步
- 練習 1:流程
- 練習 2:創造力
- 練習 3:質量
- 第二部分:簡單的黑魔法
- 練習 4:處理命令行參數
- 練習 5:cat
- 練習 6:find
- 練習 7:grep
- 練習 8:cut
- 練習 9:sed
- 練習 10:sort
- 練習 11:uniq
- 練習 12:復習
- 第三部分:數據結構
- 練習 13:單鏈表
- 練習 14:雙鏈表
- 練習 15:棧和隊列
- 練習 16:冒泡、快速和歸并排序
- 練習 17:字典
- 練習 18:性能測量
- 練習 19:改善性能
- 練習 20:二叉搜索樹
- 練習 21:二分搜索
- 練習 22:后綴數組
- 練習 23:三叉搜索樹
- 練習 24:URL 快速路由
- 第四部分:進階項目
- 練習 25:xargs
- 練習 26:hexdump
- 練習 27:tr
- 練習 28:sh
- 練習 29:diff和patch
- 第五部分:文本解析
- 練習 30:有限狀態機
- 練習 31:正則表達式
- 練習 32:掃描器
- 練習 33:解析器
- 練習 34:分析器
- 練習 35:解釋器
- 練習 36:簡單的計算器
- 練習 37:小型 BASIC
- 第六部分:SQL 和對象關系映射
- 練習 38:SQL 簡介
- 練習 39:SQL 創建
- 練習 40:SQL 讀取
- 練習 41:SQL 更新
- 練習 42:SQL 刪除
- 練習 43:SQL 管理
- 練習 44:使用 Python 的數據庫 API
- 練習 45:創建 ORM
- 第七部分:大作業
- 練習 46:blog
- 練習 47:bc
- 練習 48:ed
- 練習 49:sed
- 練習 50:vi
- 練習 51:lessweb
- 練習 52:moreweb