为正则表达式编写解析器

即使经过多年的编程,我也很惭愧地说,我从来没有真正完全掌握正则表达式。一般来说,当一个问题需要正则表达式时,我通常可以(在大量引用语法之后)想出一个合适的方法,但是我发现自己越来越频繁地使用这种技术。

因此,为了自学和理解正则表达式 适当地,我决定做我在学习时经常做的事情; 也就是说,尝试写一些雄心勃勃的东西,一旦我觉得自己学到了足够的东西,我可能就会放弃。

为此,我想用 Python 编写一个正则表达式解析器。在这种情况下,“学得足够多”意味着我想实现一个能够完全理解 Perl 扩展的正则表达式语法的解析器。然而,它并不一定是最有效的解析器,甚至在现实世界中也不一定是可用的。它只需要正确地匹配或不匹配字符串中的模式即可。

问题是,我该从哪里开始?除了在某种程度上涉及到有限状态自动机这一事实之外,我对正则表达式是如何解析和解释的几乎一无所知。如能就如何解决这一令人望而生畏的问题提出任何建议,我们将不胜感激。

编辑: 我应该澄清一下,虽然我将使用 Python 中的 执行正则表达式解析器,但我并不过分关心示例或文章是用什么编程语言编写的。只要它不在 Brainfuck,我可能就会对它有足够的了解,让它值得我花时间。

29583 次浏览

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

但是如果你对如何实现它感兴趣,即使你不能理解足够的细节来实现它,我建议你至少看看这篇文章:

正则表达式匹配可以简单快速 (但在 Java、 Perl、 PHP、 Python、 Ruby 等语言中速度较慢...)

它解释了许多流行的编程语言实现正则表达式的方式对于某些正则表达式来说可能非常慢,并解释了一种稍微不同的更快的方法。本文包括一些关于所提议的实现如何工作的详细信息,包括 C 语言中的一些源代码。如果您刚刚开始学习正则表达式,那么阅读它可能有点繁重,但我认为了解这两种方法之间的区别是非常值得的。

美丽的密码中有一个有趣的章节(如果稍微短一点的话) ,由 Brian Kernighan 撰写,恰当地称为“正则表达式匹配器”。其中,他讨论了一个简单的匹配器,可以匹配文字字符,以及 .^$*符号。

正则表达式的应用: 泛函珍珠”采用了一种有趣的方法,它在哈斯克尔得到了实现,但至少有一次是 在 Python 中重新实现

开发的程序是基于一种旧的技术,将正则表达式转化为有限自动机,这使得它在最坏情况下的时间和空间界限和实际性能方面都很有效: 尽管它的简单,Haskell 实现可以与最近发布的专业 C + + 程序竞争同样的问题。

我已经给了 Mark Byers + 1——但就我记忆所及,这篇论文并没有真正说明正则表达式匹配是如何工作的,除了解释为什么一个算法很糟糕,而另一个算法要好得多。也许是链接里的什么?

我将重点介绍一种好的方法——创建有限自动机。如果你把自己限制在确定性自动机中,而且没有最小化,那么这并不太难。

我将(非常快速地)描述的是 现代编译器设计中采用的方法。

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

a (b c)* d

这些字母表示要匹配的字符。* 是通常的零次或多次重复匹配。其基本思想是根据虚线规则导出状态。状态0我们假设没有匹配的状态,所以点在前面..。

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 (b c) * d”——与状态1相同,所以我们不需要新的状态。

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

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

也就是说,您的下一个选择是要么匹配第一次重复,要么跳过重复。接下来的状态等价于状态2和状态3。这种方法的一个优点是您可以丢弃所有过去的匹配项(在“ .”之前的所有匹配项)因为你只关心未来的比赛。这通常会给出一个较小的状态模型(但不一定是最小的状态模型)。

EDIT 如果您确实丢弃了已经匹配的细节,那么您的状态描述就是从这一点开始可能出现的字符串集的表示。

就抽象代数而言,这是一种集合闭包。代数基本上是一个有一个(或多个)运算符的集合。我们的集合是状态描述的,我们的操作符是我们的转换(字符匹配)。闭集是这样一种集合,将任何运算符应用于集合中的任何成员,总会产生集合中的另一成员。集合的闭包是闭包的极小较大集合。所以基本上,从明显的开始状态开始,我们构造相对于转换运算符集是闭合的最小状态集——可到达状态的最小集。

这里的极小指的是闭包过程——可能存在一个通常被称为极小的较小的等价自动机。

考虑到这个基本思想,说“如果我有两个表示两组字符串的状态机,那么如何导出表示联合的第三个状态机”(或交集,或集合差...)并不太困难。您的状态表示将来自每个输入自动机的一个当前状态(或一组当前状态)以及可能的其他细节,而不是虚线规则。

如果你的常规语法变得复杂,你可以尽量减少。这里的基本思想相对简单。你把你所有的状态分成一个等价类或“块”。然后重复测试是否需要针对特定转换类型拆分块(状态并不真正等价)。如果一个特定块中的所有状态都可以接受相同字符的匹配,并且在这样做时到达相同的下一个块,那么它们就是等价的。

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

关于最小化,一个特别有趣的事情是,每个确定有限状态自动机都有一个精确的最小化形式。此外,Hopcrofts 算法将生成该最小形式的相同表示,而不管它是从哪个较大的情况开始的。也就是说,这是一种“规范”表示,可用于派生散列或任意但一致的排序。这意味着您可以使用最小自动机作为容器的键。

上面的 WRT 定义可能有点草率,所以在自己使用任何术语之前,请确保自己查找它们,但是如果运气好的话,这篇文章将对基本概念做一个相当快速的介绍。

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

我同意编写一个正则表达式引擎可以提高理解能力,但是您看过 ANTLR 吗?.它为任何类型的语言自动生成解析器。因此,也许您可以尝试使用 语法例子中列出的语言文法之一,并运行它生成的 AST 和解析器。它会生成一段非常复杂的代码,但是您会对解析器的工作方式有很好的理解。