此类解析器的名称,或为何不存在


27

常规解析器消耗其全部输入并生成单个解析树。我正在寻找一个消耗连续流并产生一个解析林的人[ 编辑:请参见注释中有关为什么使用该术语可能不合常规的讨论 ]。我的直觉说我不能成为第一个需要(或认为我需要)这样的解析器的人,但是我已经反复搜索了几个月,无济于事。

我认识到我可能会被XY问题所困扰。我的最终目的是解析文本流,忽略其中的大部分内容,并从识别出的部分中生成解析树流。

所以我的问题是有条件的:如果存在具有这些特征的一类解析器,那叫什么呢? 如果没有,为什么不呢? 有什么选择?也许我缺少使常规解析器执行我想要的方式的某种方式。


1
基本上,您的解析器解析单个文档并生成一个解析树,然后立即开始解析另一个文档,依此类推。我想与对单个文档应用多种解析技术相比,这种行为修改是微不足道的。因此,它缺少一个特殊的术语。
9000

3
我用Google搜索“ Parse Forest”,发现Earley Parser产生了它们。
罗伯特·哈维

7
您是否正在寻找monadic解析器组合器 -即由几个较小的解析器组成的较大的解析器。对于将一种语言的“岛”嵌入另一种语言的情况,它们非常方便。我对C#设计团队卢克·霍本的前同事对他们的好文章:blogs.msdn.com/b/lukeh/archive/2007/08/19/...
埃里克利珀

3
有一些混乱。您是说要为流中的每个文档创建一个分析树,并且它们一起构成一个分析林。那不是解析森林的通常含义。解析林是针对单个歧义文档(简化一点)的一组解析树,这些文档可以用不同的方式进行解析。这就是所有答案的含义。您的流是由被垃圾分开的许多完整文档组成的,还是仅是部分乱码的单个文档。您的文件在语法上是否正确?适当的技术答案取决于此。
2014年

1
然后忘记有关解析森林以及Earley,GLR,Marpa和派生类的所有答案。除非出现其他原因,否则它们显然不是您想要的。您的文件语法正确吗?某些解析技术可以为部分乱码的文档重新创建上下文。您对这些文件有精确的语法吗?所有人都一样吗?您是真的要解析树,还是将文件隔离,然后在以后分别解析它们,是否会感到满意?我想我知道有什么可以改善您的处理程序的,但是我不确定您能否将其付诸实践。
2014年

Answers:


48

在使用完整个输入之前返回(部分)结果的解析器称为增量解析器。如果语法中的局部歧义只能在输入中稍后确定,则增量解析可能会很困难。另一个困难是伪装解析树中尚未到达的那些部分。

一个返回所有可能的解析树的林的解析器,也就是为模棱两可的语法的每个可能派生返回一个解析树,被称为……我不确定这些东西是否有名称。我知道Marpa解析器生成器可以做到这一点,但是任何基于Earley或GLR的解析器都应该可以实现。


但是,您似乎不需要任何这些。您有一个包含多个嵌入式文档的流,其间存在垃圾:

 garbagegarbage{key:42}garbagegarbage[1,2,3]{id:0}garbage...

您似乎想要一个跳过垃圾的解析器,(懒惰地)为每个文档生成一系列AST。这可以被认为是在其最一般意义上的增量解析器。但实际上您将实现如下循环:

while stream is not empty:
  try:
    yield parse_document(stream at current position)
  except:
    advance position in stream by 1 character or token

parse_docment然后,该函数将是常规的非增量解析器。确保成功阅读足够的输入流有一个小困难。如何处理此问题取决于您使用的解析器的类型。可能包括在某些解析错误上增加缓冲区,或使用惰性标记。

由于您的输入流,惰性标记化可能是最优雅的解决方案。解析器将懒惰地从lexer回调[1]请求下一个令牌,而不是让lexer阶段生成固定的令牌列表。然后,词法分析器将根据需要消耗尽可能多的流。这样,解析器仅在达到流的实际末尾或发生真正的解析错误时(即我们在仍处于垃圾状态时开始解析)时才会失败。

[1]在其他情况下,回调驱动的词法分析器也是一个好主意,因为这可以避免最长令牌匹配的一些问题

如果知道要搜索的文档类型,则可以优化跳过以仅在有希望的位置停止。例如,JSON文档始终以字符{或开头[。因此,垃圾是任何不包含这些字符的字符串。


5
您的伪代码实际上是我一直在做的事情,但我认为这只是一个丑陋的破解。解析器引发两种异常(NO_MATCHUNDERFLOW),这些异常使我能够区分是应该提高流位置还是等待更多输入。
凯文·克鲁姆维德2014年

5
@Kevin:我也使用此功能以及一些安全功能,以专有格式处理来自网络的传入数据。没什么好说的!
莫妮卡(Monica)

5

执行此操作的解析器没有一个特定的名称。但是,我将重点介绍一种实现此目的的算法:使用导数解析

它消耗输入,一次输入一个令牌。输入结束时将生成一个解析森林。另外,您也可以在解析过程中获得整个解析森林(部分解析)。

使用派生词进行解析可处理上下文无关的语法,并将为模棱两可的语法生成一个解析森林。

确实,这是一个优雅的理论,但是还处于起步阶段,并未得到广泛应用。Matt Might在Scala / Racket / etc中提供了各种实现的链接列表。

如果从衍生词的识别开始(即从获取语言的衍生词开始,目的是识别一些输入以确定它是否有效),然后更改程序以使用衍生词进行解析,则该理论更容易学习。也就是说,对其进行更改,而不是采用语言的派生方法,而是采用解析器的派生方法,并计算一个解析森林。


4
Downvoter:您能解释一下什么值得当选吗?如果我需要修复或改进某些内容,一定很高兴知道。
Cornstalks 2014年

我不是拒绝投票的人,我也不会梦想不加评论就拒绝投票。但是,关于复杂性和解析森林,您的热情论文没有提及许多实现相同结果的现有解析器。函数式编程很棒,但是将结果与有关该主题的现有文献进行比较也很不错。您的解析林进一步使用有多方便?
babou 2014年

@babou:作为记录,我不是该博客/论文的作者。但是,是的,我同意我可以添加更多将此算法与其他算法进行比较的详细信息,并对其进行详细说明。马特·威特(Matt Might)对此进行了完整的演讲,但是将其合并到此答案中将是很好的。如果我有时间,我将尝试扩展此答案。
Cornstalks 2014年

1
不要花太多时间来扩展它。据我所知,这不是OP所追求的。他的问题需要仔细阅读。他对解析森林的使用不是您的。--关于派生……听起来它一定很有趣,但是必须将它与以前的工作联系起来……并且其中有很大一部分。但我的意思不是这个答案,而是M Might或他的博客的论文。
2014年

2

远非理想,但我已经看到它做了不止一次:在每条输入行尝试解析。如果失败,请保留该行并添加下一个。用伪代码:

buffer = ''
for each line from input:
    buffer = buffer + line
    if can parse buffer:
        emit tree
        buffer = ''

最大的问题是,在某些语言中,在读取下一行之前,您不知道表达式是否完整。在这种情况下,您似乎可以阅读下一个,然后检查它是一个有效的开始还是一个有效的延续...但是,为此,您需要确切的语言语法

更糟糕的是,在那些语言中,创建一个直到文件末尾都无法解析的病态案例并不困难,即使这不是一个长语句。


0

简而言之

看来,解决您问题的快速方法是定义一个REGEX或FSA(有限状态自动机),该功能可以识别所有可能的文档开头(允许出现误报,但实际上与文档不符)。然后,您可以非常快速地在输入中运行它,以识别下一个可以以很少的错误开始文档的位置。它可能会导致一些错误的文档开始位置,但是解析器将识别它们并将其放弃。

因此,有限状态自动机可能是您正在寻找的解析器名称。:)

问题

总是很难理解一个实际问题,尤其是当词汇可能有很多解释时。词法分析林是为具有多个分析树的模棱两可的句子的上下文无关(CF)语法分析而创造的(afaik)。可以将其概括为解析句子的格点或其他类型的语法。因此,有关Earley,GLR,Marpa和派生解析器(还有很多其他解析器)的所有答案在这种情况下均不相关。

但这显然不是您要考虑的。您想解析一个唯一字符串,该字符串是一系列明确的文档,并为每个或某种结构化表示获取一个解析树,因为您并未真正说出文档的语法是如何定义的,从何而来正式的语言观点。您所拥有的是一种算法和表格,它们将从文档的开头开始进行解析。随它吧。

实际的问题是您的文档流包含大量垃圾,这些垃圾将文档分隔开。而且看来您的困难在于足够快地扫描此垃圾。您当前的技术是从头开始,尝试从第一个字符开始扫描,并在出现故障时跳到下一个字符重新开始,直到扫描完整个文档为止。然后,从刚扫描的文档后的第一个字符开始重复说明。

这也是@amon在回答第二部分中建议的解决方案。

这可能不是一个非常快速的解决方案(我无法测试),因为解析器的代码不太可能被优化为在文档的开头非常有效地启动。在正常使用中,它只会执行一次,因此从优化的角度来看,它不是热点。因此,您对此解决方案的满意程度并不奇怪。

因此,您真正需要的是一种可以快速找到以大量垃圾开头的文档开头的算法。您很幸运:这样的算法确实存在。而且我敢肯定,您知道的:这称为搜索REGEX。

简单的解决方案

您要做的是分析文档的规范,以查找这些文档的开始方式。我无法确切告诉您如何进行,因为我不确定它们的语法规范是如何正式组织的。可能它们都以有限列表中的某个单词开头,可能还混有一些标点符号或数字。那是你要检查的。

您要做的是定义一个有限状态自动机(FSA),或者等效地,对于大多数程序员来说,一个正则表达式(REGEX)可以识别文档的前几个字符:越多越好,但是不一定要很大(可能会占用时间和空间)。从文档的说明中应该比较容易做到这一点,并且可以使用读取文档说明的程序自动完成此操作。

生成正则表达式后,可以在输入流上运行它,以非常快速地到达第一个(或下一个)文档的开头,如下所示:

我假设:
- docstart是一个与所有文档开头匹配的正则表达式
- search(regex, stream)是一个搜索的stream子字符串的函数regex。返回时,该流将从第一个匹配子字符串的开头开始减少到其后缀子流,或者减少到找不到匹配项的空流。
- parse(stream)尝试从流的开头(剩下的内容)开始解析文档,然后以任何格式返回解析树,否则将失败。当返回时,该流从紧接在解析文档结束之后的位置开始,被缩减为其后缀子流。如果解析失败,它将调用异常。

forest = empty_forest
search(docstart, stream)
while stream is not empty:
  try:
    forest = forest + parse(stream)
  except
    remove first character from stream
  search(docstart, stream)

请注意,必须删除第一个字符,以便下一次搜索不会再次找到相同的匹配项。

当然,缩短流是一幅图像。它可能只是流上的索引。

最后一点是,您的正则表达式不需要太准确,只要它能识别所有开头即可。如果它偶尔识别出不能作为文档开头的字符串(误报),则唯一的代价就是对解析器进行一次无用调用的代价。

因此,如果有用的话,可能有助于简化正则表达式。

关于更快解决方案的可能性

上述解决方案在大多数情况下应该可以很好地工作。但是,如果您确实要处理大量垃圾和TB级文件,则可能还有其他算法运行速度更快。

这个想法源于Boyer-Moore字符串搜索算法。该算法可以非常快速地在流中搜索单个字符串,因为它使用了对字符串的结构分析,从而跳过了大部分流的读取,甚至不看片段就跳过了片段。对于单个字符串,这是最快的搜索算法。

Thr的困难在于,它要适应搜索正则表达式而不是单个字符串,这看起来非常微妙,并且可能无法正常工作,具体取决于您考虑的正则表达式的功能。这可能反过来取决于您正在解析的文档的语法。但是不要对此太信任我,因为我没有时间仔细阅读发现的文档。

我要给您留下我在网上找到的一个两个指针,其中包括一个显然是参考性的研究论文,但只有在您遇到严重的性能问题时,才应将其视为更具推测性,可能是研究性的指针。而且可能没有架子程序可以做到这一点。


-2

您所描述的内容可以描述为SAX与SOM。

SAX-(XML的简单API)是由XML文档的XML-DEV邮件列表开发的事件顺序访问解析器API。

SOM-(XML模式对象模型)对XML文件在内存中的表示形式的随机访问

C#和Java都有两种类型的实现,可能还有更多。通常,XSD或DTD是可选的。

SAX的乐趣在于它的内存开销很低,这对于大型XML文件而言非常有用。需要权衡的是,使用SAX进行的随机访问不存在或速度较慢,并且与SOM相比,更糟糕的是开发时间通常要长得多。SOM的明显问题是可能需要大量RAM。

此答案不适用于所有平台和所有语言。


1
您为什么认为OP正在解析XML?
Dan Pichelman 2014年

1
这不能回答问题。

@Snowman到目前为止,几乎没有人回答这个问题,包括已接受答案的前半部分。选择任何人都没有意义。这个问题需要仔细阅读。
2014年

@babou我没有选择任何人,我在解释我的不赞成投票。

@Snowman 解释了我的反对意见。那是公平的,我希望更多的用户会这样做。我不是母语人士:挑剔他可能是一种强烈的表达。只是每个人都做出了毫无根据的假设。因此,这甚至不值得关注。的确,这似乎比其他人多了一些。
babou 2014年
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.