编写正则expression式的parsing器
即使经过多年的编程,我也感到羞愧,说我从来没有真正掌握正则expression式。 一般来说,当一个问题需要一个正则expression式的时候,我通常可以(在引用了一些语法之后)提出一个正确的问题,但这是一种我经常使用的技术。
所以,要正确地教导自己, 正确理解正则expression式,我决定在尝试学习某些东西时做我总是这样做的事情; 即尝试写一些雄心勃勃的东西,一旦我觉得我已经学到了足够的东西,我可能会放弃。
为此,我想在Python中编写一个正则expression式parsing器。 在这种情况下,“充分学习”意味着我想实现一个parsing器,它可以完全理解Perl的扩展正则expression式语法。 但是,它不一定是最有效的parsing器,甚至不一定在现实世界中可用。 它只需要匹配或不匹配string中的模式。
问题是,我从哪里开始? 除了涉及有限状态自动机的事实之外,我几乎不了解正则expression式是如何parsing和解释的。 任何build议如何处理这个相当艰巨的问题将不胜感激。
编辑:我应该澄清的是,虽然我要在Python中实现正则expression式parsing器,但我并不过分地讨论编写这些示例或文章的编程语言。只要它不在Brainfuck中,我可能会理解得足够多它使它值得我的时光。
写一个正则expression式引擎的实现确实是一个相当复杂的任务。
但是如果你对如何做到这一点感兴趣,即使你不能够了解足够的细节来实际实现它,我也会build议你至less看一下这篇文章:
正则expression式匹配可以简单而快速 (但是在Java,Perl,PHP,Python,Ruby等方面很慢)
它解释了有多lessstream行的编程语言以一种对于一些正则expression式来说可能非常慢的方式来实现正则expression式,并解释了一种稍微不同的更快的方法。 这篇文章包含了一些关于如何实现的实现的细节,包括C中的一些源代码。如果你刚刚开始学习正则expression式,可能会有点沉重,但是我认为非常值得了解两者之间的区别方法。
我已经给Mark Byers +1了,但是据我所知,这篇文章并没有真正说明正则expression式匹配的工作原理,除了解释为什么一个algorithm不好,另一个好得多。 也许在链接中的东西?
我将专注于这个好方法 – 创build有限自动机。 如果你把自己限制在没有最小化的确定性自动机上,这并不是太困难。
我将(很快)描述的是现代编译器devise中采用的方法。
想象一下,你有以下正则expression式…
a (bc)* d
字母代表文字字符匹配。 *是通常的零次或多次重复匹配。 基本思想是基于虚线规则导出状态。 国家零,我们将作为没有任何匹配的状态,所以点在前面…
0 : .a (bc)* d
唯一可能的匹配是'a',所以我们得到的下一个状态是…
1 : a.(bc)* d
我们现在有两种可能性 – 匹配'b'(如果至less有一个'b c'的重复),否则匹配'd'。 注意 – 我们基本上是在这里search一个有向图(search深度优先或广度优先),但我们正在search它的时候发现有向图。 假设一个广度优先的策略,我们需要对我们的一个案例进行排队以供以后考虑,但是从这里我将忽略这个问题。 无论如何,我们已经发现了两个新的州…
2 : a (bc)* d 3 : a (bc)* d.
状态3是一个结束状态(可能有多个)。 对于状态2,我们只能匹配'c',但是之后我们需要小心点的位置。 我们得到“a。(bc)* d” – 这和状态1是一样的,所以我们不需要一个新的状态。
IIRC,“现代编译器devise”中的方法是当你打一个操作符时翻译规则,以简化点的处理。 状态1将被转换为…
1 : ab c (bc)* d ad
也就是说,您的下一个选项是匹配第一个重复或跳过重复。 接下来的状态等同于状态2和3.这种方法的一个优点是,您可以放弃所有过去的比赛(“。”之前的所有内容),因为您只关心未来的比赛。 这通常会给出一个较小的状态模型(但不一定是最小的模型)。
编辑如果你放弃已经匹配的细节,你的状态描述是从这一点上可以出现的一组string的表示。
就抽象代数而言,这是一种集合闭包。 代数基本上是一个具有一个(或多个)运算符的集合。 我们的集合是状态描述,我们的操作符是我们的转换(字符匹配)。 封闭的集合是将任何运算符应用于集合中的任何成员始终产生集合中的另一个成员的集合。 closures一套是closures的最大的一套。 所以基本上,从明显的开始状态开始,我们正在构造相对于我们的转换运算符集最小的一组状态 – 最小可达状态集。
这里的最小值是指闭合过程 – 可能有一个小的等价自动机,通常被称为最小值。
有了这个基本的想法,并不难说,“如果我有两个状态机代表两组string,如何导出第三个代表联合”(或交集,或设置差异…)。 而不是虚线的规则,你的状态表示将从每个input自动机的当前状态(或当前状态的集合)和可能的附加细节。
如果你的正规语法变得越来越复杂,你可以尽量减less。 这里的基本思想比较简单。 你把你所有的状态组合成一个等价类或“块”。 然后你重复testing你是否需要对某个特定的转换types进行分块(这些状态并不等同)。 如果特定块中的所有状态都可以接受相同字符的匹配,并且这样做达到相同的下一个块,则它们是相等的。
Hopcroftsalgorithm是处理这个基本思想的有效方法。
关于最小化的一个特别有趣的事情是,每一个确定性有穷自动机恰好有一个最小forms。 此外,Hopcroftsalgorithm将产生与最小forms相同的表示forms,而不pipe它从哪个表示开始。 也就是说,这是一个“规范”的表示forms,可以用来派生散列或任意但一致的sorting。 这意味着你可以使用最小自动机作为容器的键。
上面的WRT定义可能有点草率,所以在使用之前请确保自己查看任何条款,但运气好的话可以快速地介绍基本的想法。
顺便说一句 – 看看其他的Dick Grunes网站 – 他有一个免费的PDF书的parsing技术。 现代编译器devise的第一版是相当不错的海事组织,但你会看到,第二版迫在眉睫。
本文采取了一个有趣的方法。 这个实现在Haskell中给出,但是它至less在Python中被重新实现了一次。
Brian Kernighan在“ 美丽的代码”中有一个有趣的(如果稍微简短的话)章节,被称为“正则expression式匹配器”。 在这篇文章中,他讨论了一个简单的匹配器,可以匹配文字字符和.^$*
符号。
我同意写一个正则expression式引擎会提高理解,但你看看ANTLR ??。 它自动为任何一种语言生成parsing器。 所以,也许你可以尝试使用语法例子中列出的语言语法之一,并通过它生成的AST和parsing器来运行。 它会生成一个非常复杂的代码,但是您将对分析器的工作原理有一个很好的理解。