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

练习 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”。对于()+:,字符你需要更多的正则表达式。然后,你还剩下如何处理printhello1020

一旦你确定了上述代码示例中的所有符号,你需要命名它们。你不能仅仅通过它们的正则表达式来引用它们,因为查找效率低下,也令人困惑。稍后你会发现,为每个符号提供自己的名字(或数字)可以简化解析,但现在让我们为这些正则表达式设计一些名称。我可以说def只是DEF,那么()+:,可以是LPAREN RPAREN PLUS COLON COMMA。之后,我可以将用于helloprint之类的单词正则表达式称为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)

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

研究扫描器的发生器如何工作,并将其与你编写的代码进行比较。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容