什么是 Packrat 解析?

Posted

技术标签:

【中文标题】什么是 Packrat 解析?【英文标题】:What is packrat parsing? 【发布时间】:2010-11-27 11:37:21 【问题描述】:

我知道并使用 bison/yacc。但是在解析领域,关于 Packrat 解析的讨论很多。

这是什么?值得学习吗?

【问题讨论】:

【参考方案1】:

在高层次上:

Packrat 解析器使用 parsing expression grammars (PEG) 而不是传统的 context-free grammars (CFG)。

通过使用 PEG 而不是 CFG,通常比传统的 LR parser 更容易设置和维护 Packrat 解析器。

由于他们使用memoization 的方式,Packrat 解析器通常在运行时使用比“经典”解析器(如 LALR(1) 和 LR(1) 解析器)更多的内存。

与经典的 LR 解析器一样,Packrat 解析器以线性时间运行。

从这个意义上说,您可以将 Packrat 解析器视为与 LR 系列解析器的简单性/内存权衡。与 LR 系列解析器相比,Packrat 解析器对解析器内部工作原理的理论理解更少,但在运行时使用更多资源。如果您处于内存充足的环境中,并且只想将一个简单的解析器放在一起,那么 Packrat 解析可能是一个不错的选择。如果您使用的是内存受限的系统或想要获得最佳性能,那么投资 LR 系列解析器可能是值得的。

此答案的其余部分对 Packrat 解析器和 PEG 进行了更详细的概述。

关于 CFG 和 PEG

许多传统解析器(以及许多现代解析器)使用上下文无关语法。上下文无关文法由一系列规则组成,如下所示:

E -> E * E | E + E | (E) | N
N -> D | DN
D -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

例如,第一行表示非终结符 E 可以替换为E * EE + E(E),或N。第二行表示 N 可以替换为 DDN。最后一行表示D 可以替换为任何一位数。

如果从字符串 E 开始并遵循上述语法中的规则,则可以使用 +、*、括号和单个数字生成任何数学表达式。

上下文无关文法是一种表示字符串集合的紧凑方式。他们有丰富且易于理解的理论。但是,它们有两个主要缺点。第一个是,CFG 本身定义了一个字符串集合,但没有告诉您如何检查特定字符串是否由语法生成。这意味着一个特定的 CFG 是否适合一个好的解析器取决于解析器如何工作的细节,这意味着语法作者可能需要熟悉他们的解析器生成器的内部工作,以了解对可能会出现各种语法结构。例如,LL(1) parsers 不允许左递归并需要左因子分解,而 LALR(1) 解析器需要对解析算法有所了解才能消除 shift/reduce and reduce/reduce conflicts。

第二个更大的问题是语法可以是ambiguous。例如,上面的语法生成字符串 2 + 3 * 4,但有两种方式。在一种方式中,我们基本上得到了分组 2 + (3 * 4),这就是我们想要的。另一个给我们 (2 + 3) * 4,这不是这个意思。这意味着语法作者要么需要确保语法是明确的,要么需要引入辅助语法的优先级声明来告诉解析器如何解决冲突。这可能有点麻烦。

Packrat 解析器使用了一种替代上下文无关文法的方法,称为解析表达式文法 (PEG)。在某些方面解析表达式语法类似于 CFG——它们通过说明如何从(可能是递归的)较小部分组装这些字符串来描述字符串的集合。在其他方面,它们就像正则表达式:它们涉及由描述较大结构的一小部分操作组合在一起的更简单的语句。

例如,对于上面给出的相同类型的算术表达式,这是一个简单的 PEG:

E -> F + E / F
F -> T * F / T
T -> D* / (E)
D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9

要了解这句话的含义,让我们看一下第一行。与 CFG 一样,此行表示在两个选项之间进行选择:您可以将 E 替换为 F + EF。但是,与常规 CFG 不同,这些选择有特定的顺序。具体来说,这个 PEG 可以读作“首先,尝试将 E 替换为 F + E。如果可行,那就太好了!如果这不起作用,请尝试将 E 替换为 F。如果可行,太棒了!否则,我们尝试了所有方法,但都没有成功,所以放弃吧。”

从这个意义上说,PEG 直接将如何进行解析编码到语法结构本身中。而一个 CFG 更抽象地说“一个 E 可以被以下任何一个替换”,一个 PEG 具体说“要解析一个 E,首先尝试这个,然后这个,然后这个,等等。”因此,对于 PEG 可以解析的任何给定字符串,PEG 可以完全以一种方式解析它,因为一旦找到第一个解析,它就会停止尝试选项。

PEG 与 CFG 一样,可能需要一些时间才能掌握。例如,抽象的 CFG - 以及许多 CFG 解析技术 - 左递归没有问题。例如,这个 CFG 可以用 LR(1) 解析器解析:

E -> E + F | F
F -> F * T | T
T -> (E) | N
N -> ND | D
D -> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

但是,Packrat 解析器无法解析以下 PEG(尽管后来对 PEG 解析的改进可以纠正这个问题):

E -> E + F / F
F -> F * T / T
T -> (E) / D*
D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9

让我们看一下第一行。第一行说“要解析 E,首先尝试读取 E,然后是 +,然后是 F。如果失败,请尝试读取 F。”那么它将如何尝试第一个选项呢?第一步是尝试解析 E,这将通过首先尝试解析 E 来工作,现在我们陷入了无限循环。哎呀。这称为left recursion,在使用 LL 系列解析器时也会出现在 CFG 中。

设计 PEG 时出现的另一个问题是需要正确选择有序选项。如果你来自上下文无关语法的土地,那里的选择是无序的,那么很容易不小心弄乱一个 PEG。例如,考虑这个 PEG:

E -> F / F + E
F -> T / T * F
T -> D+ / (E)
D -> 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9

现在,如果您尝试解析字符串 2 * 3 + 4 会发生什么?嗯:

我们尝试解析一个 E,它首先尝试解析一个 F。 我们尝试解析 F,它首先尝试解析 T。 我们尝试解析一个 T,它首先尝试读取一系列数字。这成功读取 2。 我们已成功读取 F. 所以我们已经成功读取了一个 E,所以我们应该在这里完成,但是有剩余的令牌并且解析失败。

这里的问题是,我们先尝试解析F,然后再解析F + E,同样先尝试解析T,然后再解析T * F。结果,我们实际上比我们可以检查的要少 less,因为我们尝试在读取较长的表达式之前读取较短的表达式。

您是否发现 CFG(具有参与歧义和优先级声明)比 PEG 更容易或更难(具有参与选择排序)主要取决于个人喜好。但许多人报告发现 PEG 比 CFG 更容易使用,因为它们更机械地映射到解析器应该做什么。与其说“这是我想要的字符串的抽象描述”,不如说“这是我希望你尝试的顺序”,这更接近于解析通常的工作方式。

Packrat 解析算法

与构建 LR 或 LL 解析表的算法相比,packrat 解析使用的算法在概念上非常简单。在较高的层次上,packrat 解析器从开始符号开始,然后依次尝试排序的选择,一次一个,直到找到一个有效的选择。当它通过这些选择工作时,它可能会发现它需要匹配另一个非终结符,在这种情况下,它会递归地尝试在字符串的其余部分匹配该非终结符。如果特定选择失败,解析器会回溯,然后尝试下一个产生式。

匹配任何一个单独的作品并不难。如果您看到一个终端,它要么匹配下一个可用终端,要么不匹配。如果是这样,那就太好了!匹配它并继续前进。如果不是,则报告错误。如果您看到一个非终结符,则(递归地)匹配该非终结符,如果成功,则在该非终结符完成匹配后的位置继续搜索。

这意味着,更一般地说,packrat 解析器通过尝试解决以下形式的问题来工作:

给定字符串中的某个位置和一个非终结符,确定从该位置开始的非终结符匹配的字符串有多少(或报告它根本不匹配。)

在这里,请注意“非终结符匹配多少字符串”的含义并没有歧义。与传统 CFG 中非终结符可能在给定位置以多种不同长度匹配不同,PEG 中使用的有序选择确保如果从给定点开始有一些匹配,则正好有一个匹配从那一点。

如果您研究过dynamic programming,您可能会意识到这些子问题可能会相互重叠。事实上,在具有k 非终结符和长度为n 的字符串的 PEG 中,只有 Θ(kn) 个可能的不同子问题:起始位置和非终结符的每种组合都有一个。这意味着,原则上,您可以使用动态编程来预先计算所有可能的位置/非终结解析匹配的表,并拥有一个非常快速的解析器。 Packrat 解析本质上就是这样做的,但使用 memoization 而不是动态编程。这意味着它不一定会尝试填充所有表条目,只会尝试在解析语法过程中实际遇到的条目。

由于可以在恒定时间内填充每个表条目(对于每个非终结符,只有有限多个产生式可以尝试固定 PEG),因此解析器最终以线性时间运行,与 LR 解析器的速度相匹配。

这种方法的缺点是使用的内存量。具体来说,记忆表可以在输入字符串的每个位置记录多个条目,需要与 PEG 的大小和输入字符串的长度成比例的内存使用量。将此与 LL 或 LR 解析进行对比,后者只需要与解析堆栈大小成比例的内存,这通常比完整字符串的长度小得多。

话虽这么说,但由于不需要了解 Packrat 解析器的内部工作原理,从而抵消了更差的内存性能。您可以阅读 PEG 并从那里获取内容。

希望这会有所帮助!

【讨论】:

我觉得T -> D* / (E)应该是T -> D+ / (E),不能有空号 很好 - 已修复!【参考方案2】:

Pyparsing是一个支持packrat解析的纯Python解析库,大家可以看看它是如何实现的。 Pyparsing 使用一种记忆技术来保存输入文本中特定位置的特定语法表达式的先前解析尝试。如果语法涉及在该位置重试相同的表达式,它会跳过昂贵的解析逻辑,只从记忆缓存中返回结果或异常。

在 pyparsing wiki 的 FAQ page 上有更多信息,其中还包括返回 Bryan Ford 关于 Packrat 解析的原始论文的链接。

【讨论】:

【参考方案3】:

Packrat 解析是一种为parsing expression grammars (PEG) 提供渐近更好的性能的方法;专门针对 PEG,可以保证linear time 解析。

本质上,Packrat 解析只是意味着缓存子表达式在测试时是否在字符串中的当前位置匹配——这意味着如果当前尝试将字符串拟合到表达式中失败,则尝试拟合其他可能的表达式可以从字符串中已经测试过的点处子表达式的已知通过/失败中受益。

【讨论】:

如果我错了,请纠正我,但是尝试在给定位置匹配几个不同的非终结符号的能力(PEG 的一个特性)也意味着无限的前瞻。这意味着您可能需要将标记化输入的重要部分保留在内存中。对吗? @Honza :这是一个经典的时间/空间权衡。您是愿意在找到正确的路径之前一个接一个地遵循 N 条路径,还是希望同时遵循 N 条路径,将每条路径都保存在内存中。无论哪种方式,如果你向前看太远,那就糟透了,如果你根本不向前看,那就没有成本了。如果我向前看 1 个令牌、2 个令牌、3 个令牌,我相信我的 2G ram lappy 不会出汗……只要你不尝试解析自然语言,你应该没问题。 如果使用lazy vals (Scala Parser Combinators),那么packrat parsing 已经实现了吗?换句话说,如果我使用lazy val 来缓存已经解析的令牌,那么我是否已经在使用packrat parsing 哦!所以他们被称为 Packrat 解析器,因为他们做缓存!?

以上是关于什么是 Packrat 解析?的主要内容,如果未能解决你的问题,请参考以下文章

什么是DNS云解析?云解析和普通解析有什么区别?

什么是DNS域名解析,怎样解析?

递归解析和权威解析的区别是什么?

什么是递归解析服务器?递归解析服务器的作用是什么?

什么是云解析?云解析有哪些特点?

域名解析的原理是什么?域名解析的流程是怎样的?