Python Final Project
Parser Combinator Library
关于Python课程的Final Project,本人综合考虑了个人兴趣、项目难度等多个方面,并结合自身水平,最终决定用Python编写Parser Combinator库。希望通过编写这一Python Parser Combinator库,能够将自己心中对于易用性、简洁性、上手容易程度都颇为合理的Parser库的理解,用Python版API较好地呈现出来。
基于以上的目标,此Parser库会以用户最简单的上手程度、最少的心智负担为主要争取目标,同时提供更为强大的、更富表现力的表达式,来完成对任何字符组合的解析任务。因此,尽管此库运用了许多函数式编程的概念,但是用户并不需要补充任何相关知识,也可较好地运用此库完成任务。
目前该库取名Gparser,是属于Gaufoo的个人项目,并开源于Github,欢迎各位指正批评。接下来的文档,将详细讲解如何使用Gparser库。
Parser Combinator 概念
什么是Parser
简单地说:给定一个字符流,比如一个字符串、一段代码、一篇文章等,输入到Parser,会把原本平坦的文本改造成有层次有内涵的结构。
什么是Parser Combinator
由于函数式语言里函数是头等公民,可以把一个函数作为另一个函数的参数,一个函数可以产生出另外一个函数。基于这样的特点,我们可以把一个Parser作为参数传给另外一个Parser,一个Parser也可以产生另外一个新的Parser。有了这样的结合性,我们就可以实现Parser Combinator。
它是一种基于递归下降原理的非常简洁的Parser实现方法。
为什么是Parser Combinator
Parser Combinator在模块化、可读性、易维护等方面有着无可比拟的优势。
举个简单的例子:就像小孩子读文章,一开始他只认识一个一个字,拿着手指一个字一个字地点着读。接着他学会了把字结合起来变成词,认识词后又学会如何识别句子(很可能是因为学会了标点符号),慢慢长大后他便一目十行,脑中自然形成了文章的概念。Parser Combinator就很像这样一步一步组合的过程,非常符合人类直觉,所以编写起来相当容易。
Get Started
- 下载
首先把项目下载下来,*nix下使用命令行:
Windows用户可自行前往Github下载。
- 安装
*nix下使用命令行:
- 使用
|
|
- 删除
要删除egg文件:
然后修改site-packages/easy-install.pth
,删除相应行。
Hello World
编写最简单的Parser:
- 将Gparser库引入到环境内,并取别名为
gp
char
函数能够解析一个字符,此处解析一个.
字符,并返回一个Parser
对象Parser
对象中的run
方法可以应用于待解析的字符串,返回State
对象。State
包含解析结果(Success | ParseError
)对象和剩余字符串(LocatedText
)对象,可以用Python中的多值匹配语法进行赋值。此处result
即为解析结果对象,restTxt
即为剩余字符串对象- 打印解析结果和剩余字符串
输出如下:
- 解析成功,返回
Success
对象,里面包含Result
对象,可供提取或者进一步的处理,如何进行进一步处理会在文档后面进行介绍 - 表明解析到的位置坐标,这里表示解析到第1行第2列的
/
处 - 用用户友好的方式展示解析到的行以及待解析字符所在位置
解析结果result
的类型可能是Success
或者ParseError
,可以通过get
方法提取解析成功结果,假设解析失败,get
方法会抛出异常:
要想运行Parser
对象进行解析,还有另外一个函数run_strict
,和run
最大的区别是:run
不进行字符串结尾(即EOF
)进行检测,而run_strict
如果解析到最后没有消耗完整个字符串,则视为解析错误。
输出结果如下:
- 解析错误返回
ParseError
对象,里面包含错误信息Excepted: <EOF>
,向用户提示解析错误的原因
编写Parser
字符串参数
String
string
函数与char
函数类似,只不过从解析一个字符变成解析一个字符串,例子(接下来的例子都将省略import
等语句,均用...
代指):
假如给定的待解析字符串不能成功解析,结果如下:
One Of
one_of
函数能够接收一个字符串参数,并能成功解析字符串中的其中一个字符:
None Of
none_of
函数解析字符串参数中不包含的字符:
Regex
接受一个r
正则表达式,若从待解析字符串头开始匹配成功,则返回成功的结果:
|
|
组合子
Many
many
函数是一个解析组合子(Parser Combinator)。它能够接受一个Parser
对象参数,并能进行0次或多次的应用此Parser
:
|
|
函数库里还有另一个类似的函数:many1
,和many
的主要区别是:many1
里的Parser
能够应用1次或以上,many
则应用0次或以上。
Skip
skip
函数同样是一个组合子,它接受一个Parser
对象参数,如果成功解析,则将结果直接抛弃:
函数库中还有skip_many
、skip_many1
,他们的效果和skip(many(SOME_PARSER))
或skip(many1(SOME_PARSER))
相同:
skip
和many
的组合:
Next
Next组合子组合两个Parser
,当第一个Parser
成功解析后,抛弃结果,然后继续解析箭头指向的第二个Parser
。左Next语法为L_PARSER >> R_PARSER
,右Next语法为L_PARSER << R_PARSER
:
当Next的方向为>>
:
当Next的方向为<<
:
实际上不止可以组合两个Parser
,对于任意数量的Parser
,都可以通过Next来进行组合,取最终流向的Parser
的应用结果为解析结果:
有时候大量组合过于繁杂,建议用括号保持可读性。另一例子:
Just
在介绍了Next组合子后,just
函数的出现才有了更加明显的意义。just
函数产生的Parser
实际上不进行任何的字符消耗,但是能将自身接受的参数当做成功解析的结果返回,和Next组合起来能够共同发挥巨大的作用:
Or
Or组合子能够组合两个Parser
对象,当第一个Parser
解析失败后不会直接报告失败,而是会尝试接着第二个Parser
。Or组合子语法为L_PARSER | R_PARSER
。Or原则上不进行回溯,即第一个Parser
失败后,第二个Parser
会从第一个Parser
的失败处继续解析。
可以结合Maybe组合子将Or组合子改造成回溯版本,Maybe组合子接受一个Parser
对象作为参数,当解析失败时会回退待解析字符串。
- 不回溯
|
|
- 回溯
|
|
Between
between
组合接收三个Parser
对象参数,忽略第一个和最后一个参数的解析结果,取中间的应用结果为解析结果,即between(p1, p2, p3)
的效果实际上与p1 >> p2 << p3
效果一致。取函数别名仅为了更明确组合后的Parser
的作用,常用于解析被括弧包围的中间值,实际可以用Next组合子替代。
Separate By
sep_by
组合子接受两个Parser
对象作为参数,其中第二个Parser
作为第一个Parser
的分隔符,组合后的Parser
将多次解析被分割开的字符串,最终返回解析成功后的列表:
|
|
与其余组合子的命名习惯相同,sep_by
有可能返回空列表,sep_by1
则会解析至少一个结果。
Chain
Chain组合子分为chain_left
和chain_right
,均接受两个Parser
作为参数,第一个参数为node
,第二个参数为op
。
类似sep_by
,op
作为分隔符将分割多个node
。与sep_by
不同的是,sep_by
的sep
作为分隔符,其解析结果将被抛弃,而op
解析成功后需要返回一个函数。该函数能够将被op
分割的node
的解析结果组合起来。最常见的op
的构造办法,是利用Next和just
:pOpStr >> just(func)
。chain_left
是从左向右组合,chain_right
则为从右向左组合。
下面用tuple演示chain_left
和chain_right
的不同:
实际上,现在看不懂Chain组合子的用法无大碍,在特定情况下它的精简和方便一定会让你印象深刻。
Token
Token是一个非常方便的组合子,能够忽略待解析字符串的前后空白符,既可以用token
函数组合出来,也可以用Parser
自带的tk
方法达到同等效果:
|
|
内置解析函数
函数库中提供少量简便的解析函数,列举如下:
函数名 | 功能 |
---|---|
space | 解析空白符 |
spaces | 解析多个空白符 |
digit | 解析数字字符 |
alpha | 解析大小写字母字符 |
number | 解析正负整数 |
eof | 解析字符串结束 |
错误信息
label
函数或者Parser
对象中的label
方法能够修改解析错误后显示的错误原因,能以更清晰明了的方式让Parser
的使用者明白解析错误原因:
|
|
Map组合(核心)
接下来将介绍Gparser库最为核心的API:Map组合表达式。
连接组合子
在GParser库中,有一个很方便的组合子:+
,可以连接多个Parser
,从而获得一组解析结果:
有时候希望忽略某个Parser
的解析结果,可以在该Parser
对象前加上~
符,这里忽略pDEF
的解析结果:
Map
GParser带来的不仅仅是+
所具备的强大组合性,更重要的是组合后的Parser
的map
方法,为多个解析结果提供了组合的可能。合理使用+
和map
,能够非常轻易地编写出简洁易懂却又强大健壮的Parser。
对于未使用+
连接的Parser
,map
为其提供了转化的能力。Parser
的map
方法接受一个函数作为参数,在未使用+
连接的情况下,该函数参数接受一个参数,转化该参数并返回。当Parser
成功解析完原始的结果,map
方法将让函数参数应用此原始结果,转化成最终结果。值得注意的是,这里的原始结果是相对的,一个应用过map
的Parser
的解析结果,可以作为下一个map
的原始结果,即,Parser
可以组合多个map
。简单例子如下:
当map
结合上+
,map
接受的函数参数的参数数量将发生变化。假设+
连接了3个无前置~
的Parser
,则组合后的整个Parser
的map
方法接受的函数参数应该是一个三参函数,且顺序与+
连接的Parser
顺序一一对应。例子如下:
大部分情况下,只是需要把所有用+
连接的string
结果拼接起来,Gparser库提供了简便的concat
函数,使用如下:
对于带有~
的+
组合表达式,~
所连接的Parser
将不占用map
函数参数的参数位:
注意
Parser
内含有or_not
方法,在Map表达式中使用要十分留意,非常容易出错。若+
连接的表达式中含有or_not
,其后的map
的函数参数只能使用可变长参。
Parser实战
四则运算表达式
有关详细的四则运算表达式形式化表达可以看我以前写的这篇文章,这里只进行简单的回顾。
四则运算表达式有三个抽象组合而成,分别是Exp
、Factor
、Term
:
- Exp = Factor (( ‘+’ | ‘-‘ ) Factor)*
- Factor = Term (( ‘*‘ | ‘/‘ ) Term)*
- Term = <数字> | ‘(‘ Exp ‘)’
从以上表达式中可以看出,四则运算表达式是递归表达的,那么GParser能不能胜任这种递归表达呢?答案是肯定的。
Number
|
|
Operator
|
|
Abstraction
由于Python不能够直接引用未在之前出现过的参数,这样使得编写递归定义的Parser
比较棘手。GParser提供了undef
函数作为解决方案,它能够提前声明Parser
以占位变量,可在后续中利用assign
方法进行赋值。
|
|
这样就完成了pExp
这一四则运算表达式的Parser
,下面尝试运行:
完整代码
|
|
JSON
ADT
|
|
常量
|
|
json对象
|
|
运行:
完整代码
|
|