Elixir中的解析器组合者

2021-04-06 23:08:50

解析器组合器是解析的最有用工具之一。与正则表达式相比,它们更具可读性和可维护性,使其成为更复杂任务的绝佳选择。

本文有两部分。首先,我解释了解析器组合者如何工作以及它们所做的。之后,我将通过在Elixir编写的解析器组合库库中使用nimbleparsec进行CSV解析器来指导您完成CSV解析器。

在这一部分中,我将简要描述解析器组合器,我们将尝试从头开始构建功能解析器组合器。我们将制作的组合者将是低级,更糟糕的是您将使用简单的正则表达式达到;他们在那里说明了这一点。

如果您想在操作中看到解析器组合器,请直接到NimbleParsec部分。

编程时,我们经常必须将输入(如字符串)解析为更友好的数据结构(如树或列表)。

一种快速的方法是编写一个正则表达式,捕获我们需要的一切。但这些可能变得非常冗长和复杂,这导致了丑陋的代码。

如果我们可以将解析器写入输入中的一对一的解析器,而是何时何时何时映射到输入中,并将它们组合为该输入进行解析器?

最终,解析器组合器就是这样:一种方式来组合简单的解析器来创建更复杂的解析器。

那么解释者确实是什么?解析器的主要目标是将一串文本解析为不同的,更多的结构化对象,如列表或树。

例如,我们可以接受整数列表作为字符串" 3,1,1,4,13;并将该字符串转换为列表以更好地表示字符串中固有的结构 - [3,1,4,1]。

但是,如果我们遇到像" 3,1.1,4,12,星期一,12月28日&#34 ;?或"哎呀,我'抱歉&#34 ;?要与其他解析器进行撰写并处理可能的故障,我们还需要返回其余的输入如果解析器成功,如果它没有错误,则错误。

这是一个低级解析器的示例,它在Elixir中解析了一个十进制数字。

def(<<< char,strite :: bit.>)当char> = 48和char< = 57,do:{:确定,[char - 48],rest} def(_),做: {:错误,:错误_INPUT}

如果您询问我们可以用它做什么,答案是:不是很多。 😅解锁解析器组合者的力量,我们需要找到一种方法来将不同的解析器放在一起。

解析器组合器是将两个或多个解析器组合到另一个解析器中的函数。

让我们考虑我们可以结合解析器的方式。最简单的组合将是将两个链接在一起 - 使解析器逐个解析两个十进制数字。

def(有趣,fun2)做fn x - >案例有趣。(x)do {:好的,解析,休息} - >案例fun2。(休息)do {:好的,parsed2,rest2} - > {:好的,解析++ parsed2,rest2} err - >呃结束错误 - >呃结束结束

这里,得到的解析器将第一函数应用于输入,然后将第二个功能与第一函数返回的剩余输入的其余部分。我们将两个解析的项目作为列表返回,第二个功能未消耗的输入。如果出现错误,它就刚刚进一步传递。

现在我们可以反复使用Combinator来创建一个可以在一行中解析2,3,甚至4和更多整数的解析器!但这只是一开始。

那里有多种其他组合器可能性。频繁的是选择,一个天真的版本可以如下所示:

def(fun1,fun2)do fn x - >案例{fun1。(x),fun2。(x)} do {{:好的,解析,休息},_} - > {:好的,解析,休息} {_,{:确定,解析,休息}} - > {:好的,解析,休息} {err,_} - >呃结束结束

在这里,它将尝试一个接一个地解析两个不同的解析器并选择首先或返回错误的那个。

def()do fn x - > parse_digit(x)结束ode def()do digit_parser()|> concat(digit_parser())结束def()do digit_parser()|> concat(digit_parser())|> concat(digit_parser())结束def()do选择(three_digits(),two_digits())结束

通过组合不同的解析器,您可以构建代表JSON或XML等语言规则的大型复杂解析器。

Real Parser Combinator库通常提供各种不同的组合器,使得可以以可读方式表示解析器。我们会在我们的NimbleParser示例中看到。

我们的初步错误处理是难度的,并且我被告知有一种误解,解析器组合者严重处理错误。让我们看看我们如何轻松扩展Parser以显示意外输入的位置。

def(<< char,休息:: bit.>)当char> = 48和char< = 57,DO:{:OK,[CHAR - 48],REST} DEF(<< ,_rest :: bitstring>>),do:{:错误,{:意外的,<<> char>> 1}} def(""),do:{:错误, :end_of_string} def(_),do:{:错误,:not_string}

除了输入错误之外,EOS错误也会很容易地发生,因此我确保覆盖。

现在我们可以修改芯片组合器以跟踪输入错误的位置,如果发生?

def(有趣,fun2)做fn x - >案例有趣。(x)do {:好的,解析,休息} - >案例fun2。(休息)do {:好的,parsed2,rest2} - > {:好的,解析++ parsed2,rest2} {:错误,{:意外_ input,输入,pos}} - > {:错误,{:mementrial_input,输入,string.length(x) - string.length(rest)+ pos}} Err - >呃结束错误 - >呃结束结束

选择组合器已经很好地处理这些错误。您可以在此处看到最终结果。

现在,当我们尝试做两个_or_three_digits时。(" 5a"),我们会得到{:错误,{:mementrial_input," a",2}}。如果我们将代码视为库,我们可以轻松地制作错误消息。

当然,此代码仅用于演示目的,但是在Megaparsec中使用了类似的方法,该方法是众所周知的Haskell解析器组合库库,该库被着名,该库是由于其不错的错误报告。

由于Parser组合器比Regex更强大,因此您可以使用它们来解析具有复杂,递归结构的项目。但是,它们也可以用于简单的解析器,其中一个项目可以具有很多不同的替代方案。

但是,它们不会替换正则表达式。每个工具都有它的好处。对于大多数其他解析需求,我将使用Regex为简单的脚本或单行组和解析器组合器。

快速抛开:Jonn Mostovoy在此部分中得到了很多帮助,他最近发表了一台实践指南,以在Rust中使用Parser Combinators。如果您有兴趣看到如何以裸露的金属语言处理它们,我建议检查它。

nimbleparsec是一个使用元标记的库,为您提供编译成二进制模式匹配的高效解析器。在本节中,我们将使用它来构建一个简单的CSV解析器,将采用CSV文件并将其转换为列表列表。

首先,让我们使用mix new csvparser创建一个名为csvparser的新项目。之后,添加{:nimble_parsec,"〜> 1.0"}在MIX.EXS中的依赖项列表和模块中导入NIMBLEPARSEC。

CSV由行组成,每个线条由逗号分隔的值组成。我们可能可以定义CSV值,然后使用定义来定义一行。让我们用英语写出简单的定义。

值是一个字符串(允许现在忽略数字,转义字符和浮动)。

线由一个值组成,然后可能的重复(逗号,然后值),然后是EOL字符。

我们如何使用库中可用的功能来反映这个简单的语法?

要实现值,我们需要考虑将分开这些值的字符。一个好的竞争者是,但你也可以遇到换行符\ n和\ r。值也可以是空的,所以我们需要提供它。

最适合我们的目标是UTF8_STRING,这让我们提供了几个论点,例如不是(不解析的字符)和最小长度)。

然后,我们需要定义一行。对于我们,一行是一个值,然后是逗号和一个值,重复0或更多次,然后重复为EOL字符。

我们有值定义,但让我们快速定义涵盖Windows,MacOS和Linux的EOL解析器。

正如我们之前所看到的,选择使我们能够解析从功能列表中成功的第一个选项。

之后,我们可以使用Combinator忽略,Concat和与我们定义的解析器一起重复以定义一行。

忽略将忽略字符并在不解析任何内容的情况下向前移动,求解两个解析器,并重复重复解析器,直到它不会成功。

现在我们有线元素,它非常容易定义完整的解析器。为此,我们需要使用defparsec宏。

在这里,我们解析一行,将其包装在[]中,然后重复该过程,直到它没有成功。现在,如果我们读取CSV文件,CSVParser.file(file_contents)将解析简单的CSV文件的内容。

defmodule do导入nimbleparsec值= utf8_string([不是:\ r,不是:?\ n,不是:?,],min:0)eol = choice([string(" \ r \ n") ,字符串(" \ n")])线=值|>重复(忽略(串(""))|> concat(价值))|>忽略(eol)defparsec:文件,线|>包裹()|>重复(),调试:TRUE END

我们的CSV定义非常简单。它没有涵盖在CSV中出现的一些东西,它也可以处理相同的数字和字符串。可以说,我们可以将文件拆分在换行符上,在生成的列表上映射逗号拆分,并实现了相同的结果。

但是,由于我们创建了一个良好的基础,因此向解析器添加新定义比改善双线功能要简单得多。让我们尝试立即解决这些问题之一。

其中一个问题是CSV文件可以在条目中具有逗号。我们的解析器始终在逗号上拆分。让我们通过将条目包装在双引号中添加一个选项来逃脱逗号。

转义的值由零个或多个字符组成,由双引号包围。如果逃逸值内部的双重引用,则需要通过另一个双重报价进行双引用。

然后我们需要弄清楚如何解析双引号的内部以满足要求。

经过一个相当环形交流的方式实现这一目标(您不想知道🙈),我发现一个在真实世界中的提示Haskell,我们可以逐一读取项目的字符,连续两个双引号匹配或非报价字符。

现在我们可以在Escaped_character上使用重复组合器,然后加入我们解析的所有字符。

让我们将原始值重命名为ramence_value,并在ESCAPED_VALUE和REMINAL_VALUE之间进行值。

escaped_character = choice([字符串(" \" \""" [34; [不是:"],1)])commonal_value = utf8_string([不是: ?\ r,不是:?\ n,不是:?,],min:0)escaped_value =忽略(字符串("""))|>重复(escaped_character)|>忽略(字符串(" \""))|>减少({enum,:加入,[""]})值=选择([escaped_value,scround_value])

我们需要先将Escaped_value放进,因为否则,在我们的字符串上有机会逃脱之前,解析器将在range_value上取得成功。

当然,可以进一步改善这个解析器。例如,您可以为额外的空白或数字添加支持,这是一个令人兴奋的练习。

我希望这是一个令人兴奋的旅程,你今天学到了新的东西! 如果您想了解更多关于Elixir的信息,欢迎您浏览我们的Elixir文章并在推特上关注我们,每当我们发布新的时,DED或Medio接收更新。