为正则表达式编写解析器


73

即使经过多年的编程,我还是很to愧地说我从未真正完全掌握过正则表达式。通常,当问题需要使用正则表达式时,我通常可以(在一堆引用语法之后)提出一个合适的表达式,但这是我发现自己使用频率越来越高的一种技术。

因此,为了自学并正确理解正则表达式,我决定做一些尝试学习的事情;即,尝试写一些雄心勃勃的东西,一旦我学到了足够的东西,我可能会放弃。

为此,我想用Python编写一个正则表达式解析器。在这种情况下,“足够了解”意味着我想实现一个解析器,该解析器可以完全理解Perl的扩展正则表达式语法。但是,它不一定是最有效的解析器,甚至不一定在现实世界中可用。它仅必须正确匹配或不匹配字符串中的模式。

问题是,我从哪里开始?除了正则表达式在某种程度上涉及有限状态自动机,我几乎对正则表达式的解析和解释一无所知。对于如何解决这个相当艰巨的问题的任何建议将不胜感激。

编辑:我应该澄清一下,尽管我将在Python中实现正则表达式解析器,但我并没有对示例或文章所使用的编程语言感到过分困惑。只要不在Brainfuck中,我可能就足够理解了它使它值得我的时间。


1
+1有趣的想法。如果您这样做,您将是正则表达式的专家;)
拜伦·惠特洛克

2
关于如何构建简化的RE解析器(虽然与Python不相关)的有趣文章
systempuntoout 2010年

2
perl.plover.com/Regex/article.html是使用自动机的正则表达式引擎的说明。您可能还想考虑一个较简单的项目,该项目是在不久前在这里提出的,那就是编写一个正则表达式到英语的翻译器。例如,(foo|bar)(baz)+应翻译为either "foo" or bar" then one or more "baz"。Pyparsing(pyparsing.wikispaces.com/Documentation)可能对此有所帮助。
卡特里尔2010年

2
好吧,我将轻松开始并为正则表达式实现解析器(不是perl风格的regexp,而是原始类型)。如果我没有记错的话,“ Beautiful Code”一书中的第一章是一个很好的优雅的正则表达式解析器实现。尽管它是C语言,而不是Python语言,但它仍然是一个不错的起点。
麦克风” 2010年

1
@Chinmay:出于好奇:它是怎么回事?您是否曾经编写过正则表达式解析器?它花了多少时间?奏效了吗?考虑自己做。
jackthehipster 2014年

Answers:


42

编写正则表达式引擎的实现确实是一项非常复杂的任务。

但是,如果您对如何实现感兴趣,即使您对实际实现的细节不够了解,我还是建议您至少看一下这篇文章:

正则表达式匹配可以简单又快速(但在Java,Perl,PHP,Python,Ruby等中却很慢)

它解释了有多少种流行的编程语言以某种正则表达式可能非常慢的方式实现正则表达式,并解释了一种稍有不同的更快的方法。本文包含有关拟议实现的工作原理的一些详细信息,包括C中的一​​些源代码。如果您刚刚开始学习正则表达式,可能会有些繁琐,但是我认为值得了解两者之间的区别是很值得的方法。


3
这是一篇令人难以置信的文章。我已经快到一半了,我已经看到代码在脑海中成形!
Chinmay Kanchi 2010年

3
@Chinmay Kanchi:该文章的作者还写了其他一些有关正则表达式的文章。这也是一个非常有趣的地方:swtch.com/~rsc/regexp/regexp3.html,并详细介绍了如何实现大多数现代正则表达式引擎支持的一些更高级的功能。
Mark Byers 2010年

1
我将接受这个答案,因为它是教给我最多知识的人。干杯!
Chinmay Kanchi 2010年

21

我已经给Mark Byers +1了-但是据我所知,除了解释为什么一种算法不好而另一种更好的算法外,关于正则表达式匹配的工作原理并没有说太多。也许链接中有东西?

我将专注于好的方法-创建有限的自动机。如果您将自己限制在确定性的自动机上而没有最小化,那么这并不是很难。

我将(很快)描述的是Modern Compiler Design中采用的方法。

假设您有以下正则表达式...

a (b c)* d

字母表示要匹配的文字字符。*是通常的零个或多个重复匹配。基本思想是基于虚线规则导出状态。我们将零状态设为尚未匹配的状态,因此点位于最前面...

0 : .a (b c)* d

唯一可能的匹配是'a',因此我们得出的下一个状态是...

1 : a.(b c)* d

现在,我们有两种可能性-匹配“ b”(如果至少有一个“ b c”重复)或匹配“ d”。注意-我们基本上是在这里进行有向图搜索(深度优先或广度优先等等),但是在搜索时会发现有向图。假设采用广度优先的策略,我们需要将其中一种情况排入队列,以供以后考虑,但从现在开始,我将忽略该问题。无论如何,我们发现了两个新状态...

2 : a (b.c)* d
3 : a (b c)* d.

状态3是结束状态(可能不止一个)。对于状态2,我们只能匹配“ c”,但是之后需要注意点的位置。我们得到“ a。(bc)* d”-与状态1相同,因此我们不需要新的状态。

IIRC,现代编译器设计中的方法是在您碰到运算符时转换规则,以简化点的处理。状态1将转换为...

1 : a.b c (b c)* d
    a.d

也就是说,您的下一个选择是匹配第一个重复或跳过重复。此状态中的下一个状态等效于状态2和3。此方法的优点是,您可以丢弃所有过去的比赛(“。”之前的所有内容),因为您只关心将来的比赛。这通常会提供较小的状态模型(但不一定是最小的状态模型)。

编辑如果您确实放弃了已经匹配的详细信息,那么状态描述就是从那时起可能出现的一组字符串的表示。

在抽象代数方面,这是一种集合闭包。代数基本上是具有一个(或多个)运算符的集合。我们的集合是状态描述,而我们的运算符是我们的过渡(字符匹配)。封闭集是将任何运算符应用于集合中的任何成员时始终产生集合中另一个成员的集合。集合的闭合是闭合的最小的较大集合。因此,基本上,从明显的起始状态开始,我们正在构造相对于过渡运算符集闭合的最小状态集-可达状态的最小集。

最小是指关闭过程-可能会有一个较小的等效自动机,通常称为最小自动机。

牢记这一基本思想,说“如果我有两个代表两组字符串的状态机,如何导出代表联合的第三个状态机”(或交集,或集差...),就不是太难了。您的状态表示法将代替每个输入自动机的当前状态(或一组当前状态),还可能附加其他详细信息,而不是虚线规则。

如果您的常规语法变得复杂,则可以将其最小化。这里的基本思想比较简单。您将所有状态分组为一个等效类或“块”。然后,您反复测试是否需要针对特定​​的过渡类型拆分块(状态实际上不是等效的)。如果特定块中的所有状态都可以接受相同字符的匹配,并且这样做时到达相同的下一个块,则它们是等效的。

Hopcrofts算法是处理此基本思想的有效方法。

关于最小化的一个特别有趣的事情是,每个确定性有限自动机都恰好具有一个最小形式。此外,Hopcrofts算法将以最小形式产生相同的表示形式,而不管它从哪个大写开始。也就是说,这是一个“规范”表示形式,可用于派生哈希值或用于任意但一致的排序。这意味着您可以使用最少的自动机作为容器的键。

上面的内容可能有点草率的WRT定义,因此请确保您在自己使用任何术语之前先查找它们,但是幸运的是,这可以快速地介绍基本概念。

顺便说一句-看看Dick Grunes网站的其余部分-他有一本关于解析技术的免费PDF书籍。《现代编译器设计》的第一版相当不错的IMO,但是正如您将看到的那样,即将有第二版。


2
此技巧与生成LR解析器的方法相同:通过语法规则集推入表示解析器状态的点。虚线规则代表解析状态。
艾拉·巴克斯特

好答案。仅供参考,与现代编译器设计的链接已断开。
rvighne '16


6

Brian Kernighan的Beautiful Code中有一个有趣的(如果稍短的话)章节,适当地称为“正则表达式匹配器”。在其中,他讨论了可以匹配文字字符和.^$*符号的简单匹配器。


2

我确实同意编写正则表达式引擎可以增进理解,但是您是否看过ANTLR ??。它会自动为任何一种语言生成解析器。因此,也许您可​​以尝试语法示例中列出的一种语言语法,并通过它生成的AST和解析器进行尝试。它生成了非常复杂的代码,但是您将对解析器的工作原理有很好的了解。


3
那会破坏目标,不是吗?
Chinmay Kanchi 2010年

好吧,实际上您可以研究它生成的代码。指南的每一行在ANTLR权威指南中都做得很好。以它为参考,并研究其在幕后使用的所有技术。至少可以学习一些技巧,这可能是一个很好的起点,这可能有助于从头开始编写正则表达式引擎。
A_Var 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.