这个教程翻译自Getting Started with Pyparsing这书,略去一些例子没翻译,但总体意思到了。
pyparsing是一个DSL的难以想象的简易实现,相信你看过开头几段就能明白它意味着什么。
-------
Pyparsing 导引
by Pau McGuire
你需要从文本文件或网页中提取数据吗?或者你想要更人性化的用户命令接口或者搜索字符串?正则表达式和lex/yacc让你的眼睛和脑袋疼?
Pyparsing将成为解决方案。Pyparsing是纯python的类库,其能让你容易的建立递归下降(recursive-descent)解析器。这不需要你自己写个解析器。只要使用pyparsinng,你能够解析HTML,轻松建立日志文档数据提取器,复杂数据结构以及命令解释器。这个Short Cut将告诉你怎么做!
目录
什么是Pyparsing?
Pyparsing程序的简单形式
"Hello World"
什么使Pyparsing变得不同?
从表中解析数据-使用Parse Action和ParseResults
从网页解析数据
一个简单的S表达式解析器
python中lambda怎么使用
一个复杂的S表达式解析器
解析搜索字符串
100行代码以内的搜索引擎
结论
索引
";我需要解析这个日志文件..."
";只是要从网页中提取数据..."
";我们需要一个简单的命令行解释器..."
";我们的源代码需要移植到新API集上..."
这些工作要求每天都让开发者们条件反射般的骂娘";擦,又要一个解析器!"
解析不十分严格格式的数据形式的任务经常出现在开发者面前。有时其是一次性的,像内部使用的API升级程序。其他时候,解析程序作为在命令行驱动的程序中的内建函数。
如果你在Python中变成,你可以脱离这些工作,通过使用Python的内建字符串方法,比如split(),index()以及startwith().
让这项工作又变得讨厌的是我们经常不只是对字符串分割和索引,对于一些内容可变形式或复杂的语法定义来说。比如:
y = 2 * x + 10  (每个符号间都有空分隔)
是容易解析的,对于这种空格分离的形式。不幸的是,很少有用户会如此这般使用空格,算术表达式经常像这样:
y = 2*x + 10
y = 2*x+10
y=2*x+10
直接对最后一个字符串运用split方法会导致返回原字符串(作为一个列表的唯一实例),不会分离出这些单独的元素y,=2,等等.
处理这种超越str.split的解析任务的工具是正则表达式或lex/yacc。正则表达式用一个字符串去描述文本模式以便匹配。那个字符串使用特殊符号(像|,+,.,*,?)去表示不同的解析概念像alternation(多选),repetition(重复)以及wildcards(通配符).Lex/yacc是则先拆出标记,然后应用过程代码到解压出的标记上。Lex/yacc使用一个单独的标记定义文件,然而产生lex中间文件以及标记过程代码模板给程序员扩展,以驱动
程序的特殊行为。
历史注释
这些文本处理技术最早在1970年以C实现,现在它们仍
在广大的领域发挥作用。Python通过re模块以及"batteries included";的部分标准库提供了对正则表达式的支持。你可以下载一些免费的lex/yacc风格的解析器模块,其提供了对python的接口。
这些传统工具的主要问题在于它们独特的标记系统需要被精确映射到Python的代码上。比如lex/yacc风格工具往往要单独进行一个代码产生阶段。
实践中,解析器编写看起来陷入一个怪圈中:写代码,解析示例文本,到附加的特殊情况等等。组合正则表达式符号,额外的代码生成步骤,很这个循环过程可能会不断的陷入挫折。
Pyparsing是什么?
Pyparsing是纯python编写的,易于使用。Pyparsing的类库提供了一系列类让你可以从表达式单独的元素中开始构建解析器。其表达式使用直觉的符号组合,如+表示将一个表达式加到另一个后面。|,^表示解析多选(意为匹配第一个或匹配最长的).表达式的返回值修饰以类的形式追加,如OneOrMore,ZeroOrMore,Optional.
作为例子,一个正则表达式处理IP地址后面跟着一个US风格的电话号码需要这样写:
(\d{1,3}(?:\.\d{1,3}){3})\s+(\(\d{3}\)\d{3}-\d{4})
对比一下,类似的表达式用pyparsing写是这个样子
ipField = Word(nums, max=3)
ipAddr = Combine( ipField + "." + ipField + "." + ipField + "." + ipField )
phoneNum = Combine( "(" + Word(nums, exact=3) + ")" + Word(nums, exact=3) + "?" + Word(nums, exact=4) )
userdata = ipAddr + phoneNum
尽管它使用了更长,但pyparsing版本更易读;它更容易被回朔和更新,比如可以更容易从此迁移去处理其他国家的电话号码格式,
Python新手?
我已经收到很多邮件,他们告诉我使用pyparsing也是他们第一次使用python编程。他们发现pyparsing易于得到,而且还有好读的例子。如果你是刚开始使用python,你肯恩感到一点困难阅读这些例子。Pyparsing不要求任何高级的python只是,它易于学习。有一些网络教程资源,比如python的。
[].
为了更好的使用pyparsing,你应当更熟悉python的语言特性,如缩进语法,数据类型,以及for item in itemSequence: 式循环控制语法
Pyparsing使用object.attribute式标记,就像python的内建容器类,元组,表以及字典。
这本书的例子使用了python的lambda表达式,本质上就是单行函数;lambda表达式特别有用于定义简单的解析操作时。
列表解析和生成器表达式的迭代形式是有用的,在解析标记结果时,但并不是必须的。
Pyparsing是:
100%纯python,没有编译过的动态链接库(DLLs)或
者共享库包含其中,所以你可以使用它在任何平台在python2.3能够编译的地方。
驱动解析表达式的被内联(inline)编码,使用标准的python类标记和符号-没有单独的代码产生过程也没有
特殊符号和标记,这将是你的应用易于开发,理解和维护。
类似的表达形式:
C,C++,Java,Python,HTML注释
引号字符串(使用单个或双引号,除了\',\'';转义情况外)
HTML与XML标签(包含上下级以及属性操作)
逗号分隔以及被限制的列表表达式
轻量级封装-Pyparsing的代码包含在单个python文件中,容易放进site-packages目录下,或者被你的应用包含。
Pyparsing程序的简单形式
典型的pyparsing程序具有以下结构:
import pyparsing模块
使用pyparsing类和帮助方法定义语法
使用语法解析输入文本
处理从解析出的文本
从Pyparsing出引用名字
通常,使用from pyparsing import *是不被python风格专家鼓励的。因为它污染了本地变量命名空间,因其从不明确的模块中引入的不知道数量的名字。无论如何,在pyparsing开发工作中,很难想象pyparsing定义的名字会被使用,而且这样写简化了早期的语法开发。在语法最终完成后,你可以回到传统风格的引用,或from你需要的那些名字。
定义语法
语法是你的定义的文本模式,这个模式被应用于输入文本提取信息。在pyparsing中,语法由一个或多个Python语句构成,而模式的组合则使用pyparsing的类和辅助对象去指定组合的元素。Pyparsing允许你使用像+,|,^这样的操作符来简化代码。作为例子,假如我使用pyparsing的Word类去定义一个典型的程序变量名字,其由字母符号或字母数字或下划线构成。我将以Python语句这样描述:
identifier = Word(aphas,alphanus+'_')
我也想解析数字常数,如整数和浮点数。另一个简化过的定义的Word对象,它应当包含数字,也许还包含小数点。
number= Word(num+'.')
从这里,我然后定义一个简单的赋值语句像这样:
assignmentExpr = identifier + "=" +(identifier|number)
现在我们可以解析像这样的内容了:
a = 10
a_2=100
pi=3.14159
goldenRatio = 1.61803
E =mc2
在程序的这个部分,你可以附加任何解析时回调函数(或解析动作parse actions)或为语法定义名字去减轻之后指派它们的工作。解析动作是非常有力的特性对于pyparsing,之后我们将论述它的细节,
实践:BNF范式初步
在写python代码实现语法之前,将其先写在纸上是有益的,如:
帮助你澄清你的想法
指导你设计解析器
提前演算,就像你在执行你的解析器
帮助你知道设计的界限
幸运的是,在设计解析器过程中,有一个简单的符号系统用来描绘解析器,它被称
为BNF(Backus-Naur Form)范式.你可以在这里获得BNF的好例子:/wiki/backus-naur_form.你并不需要十分严格的遵循它,只要它能刻画你的语法想法即可。
在这本书里我们用到了这些BNF记号:
::= 表示";被定义为"
+ 表示“一个或更多”
*
表示“零个或更多”
被[]包围的项是可选的
连续的项序列表示被匹配的标记必须在序列中出现
|表示两个项之一会被匹配
使用语法解析输入文本
在早期版本的pyparsing中,这一步被限制为使用parseString方法,像这样:
assignmentTokens = assignmentExpr.parseString("pi=3.14159")
来得到被匹配的标记。
现在你可以使用更多的方法,全部列举如下:
parseString 应用语法到给定的输入文本(从定义上看,如果这个文本可以应用多次规则也只会运用到第一次上)
scanString 这是个生成器函数,给定文本和上界下界,其会试图返回所有解析结果
searchString scanString的简单风光,返回你给定文本的全部解析结果,放在一个列表中。
transformString scanString的另一个封装,还附带了替换操作。
现在,让我们继续研究parseString,稍后我将给你们展示其他选择的更多细节。
处理解析后的文本
当然,如何处理解析文本得到的返回值是最重要的。在大多数解析工具中,通常会返回一个匹配到的标记的列表供未来进一步解释使用。Pyparsing则返回一个更强的对象,被称为ParseResults.在最简单的形式中,ParseResults可以被打印和连接像python列表一样。作为例子,继续我们赋值表达式的例子,下面的代码:
assignmentTokens = assignmentExpr.parseString("pi=3.14159")
print assignmentTokens
会打印出
['pi','=','3.14159']
但是ParseResults也支持解析文本中的个域(individual fields),如果语法为返回值的某些成分指派了名字。
这里我们通过给表达式里的元素取名字加强它们(左项成为lhs,右项称为rhs),我们就能在ParseResults里连接这些域,就像它们是返回的对象的属性一样。
assignmentExpr = identifier.setResultsName("lhs") + "=" + \
(identifier | number).setResultsName("rhs")
assignmentTokens = assignmentExpr.parseString( "pi=3.14159" )
print assignmentTokens.rhs, "is assigned to", assignmentTokens.lhs
将打印出
3.14159 is assigned to pi
现在介绍进入转入细节部分了,让我们看一些例子
"Hello,World";在Steroids
Pyparsing有很多例子,其中有一个简单地"Hello World";解析器。这个简单的例子也被O'Reilly,ONLamp[
onlamp]的文章";用Python建立递归下降解析器"(
).在这一节,我也使用类似的例子以介绍简单的pyparsing解析工具。
当前"Hello,World!";的解析模式被限制为:
word, word !
这过于受限了,让我们扩展语法以适应更多的情况。比如说应当可以解析以下情况:
Hello, World!
Hi, Mom!
Good morning, Miss Crabtree!
Yo, Adrian!
Whattup, G?
How's it goin', Dude?
Hey, Jude!
Goodbye, Mr. Chips!
写一个这样的解析器的第一步是分析这些文本的抽象模式。像我们之前推荐的那样
,让我们用BNF范式来表达。用自然语言表达这个意思,我们可以说:";一个这样的句子由一个或多个词(作为问候词),后跟一个逗号,后跟一个或多个附加词(作为问候的对象)",结尾则使用一个感叹号或问好。用BNF可以这样表达:
greeting ::= salutation comma greetee endpunc
salutation ::= word+
comma ::= ,
greetee ::= word+
word ::= a collection of one or more characters, which are any alpha or ' or .
endpunc ::= ! | ?
这个BNF几乎可以直译为pyparsing的语言,通过使用pyparsing的元素Word,Literal,OneOrMore以及辅助方法oneOf。(BNF与pyparsing的一个区别在于BNF喜欢使用传统的由上自下的语法定义,pyparsing则使用由底至上的方式。因为我们要保证我们使用的元素在上面已经定义过了)
word = Word(alphas+"'.")
salutation = OneOrMore(word)
comma = Literal(",")
greetee = OneOrMore(word)
endpunc = oneOf("! ?")
greeting = salutation + comma + greetee + endpunc
oneOf使定义更容易,比较两种等价写法
endpunc = oneOf("! ?")
endpunc = Literal("!") | Literal("?")
oneOf也可以直接传入由字符串构成的列表,直接传字符串也是先以空格分离成那样的列表的
使用我们的解析器解析那些简单字符串可以得到这样的结果。
['Hello', ',', 'World', '!']
['Hi', ',', 'Mom', '!']
['Good', 'morning', ',', 'Miss', 'Crabtree', '!']
['Yo', ',', 'Adrian', '!']
['Whattup', ',', 'G', '?']
["How's", 'it', "goin'", ',', 'Dude', '?']
['Hey', ',', 'Jude', '!']
['Goodbye', ',', 'Mr.', 'Chips', '!']
每个东西都被很好的解析了出来。但是我们的结果缺乏结构。对这个解析器而言,如果我们想要提取出句子的左边部分-即问候部分,我们还需要做一些工作,迭代结果直到我们碰上了逗号:
for t in tests:
results = greeting.parseString(t)
salutation = []
for token in results:
if token == ",": break
salutation.append(token)
print salutation
很好!我
们应该已经实现了一个不错的字符-字符的扫描器。幸运的是,我们的解析器可以足够智能以避免之后繁琐工作。
当我们直到问候及问候对象是不同的逻辑部分之后,我们可以使用pyparsing的Group类来为返回结果赋予更多的结构。我们修改salutation和greetee为
salutation = Group( OneOrMore(word) )
greetee = Group( OneOrMore(word) )
于是我们的结果看起来更有组织性了:
['Hello'], ',', ['World'], '!']
['Hi'], ',', ['Mom'], '!']
['Good', 'morning'], ',', ['Miss', 'Crabtree'], '!']
['Yo'], ',', ['Adrian'], '!']
['Whattup'], ',', ['G'], '?']
["How's", 'it', "goin'"], ',', ['Dude'], '?']
['Hey'], ',', ['Jude'], '!']
['Goodbye'], ',', ['Mr.', 'Chips'], '!']
然后我们可以使用简单的列表拆包实现不同部分赋值:
for t in tests:
salutation, dummy, greetee, endpunc = greeting.parseString(t)
print salutation, greetee, endpunc
打印出:
['Hello'] ['World'] !
['Hi'] ['Mom'] !
['Good', 'morning'] ['Miss', 'Crabtree'] !
['Yo'] ['Adrian'] !
['Whattup'] ['G'] ?
["How's", 'it', "goin'"] ['Dude'] ?
['Hey'] ['Jude'] !
['Goodbye'] ['Mr.', 'Chips'] !
注意我们用dummy变量记入了解析出的逗号。这些逗号在解析中是很有用的,比如让我们分隔问候部分和问候对象部分。但在结果中我们对逗号不感兴趣,它应当从结果中消失。你可以使用Suppress对象包住逗号定义以抑制其出现。
comma = Suppress( Literal(",") )
你可以以不同的等价方式表达以上语句
comma = Suppress( Literal(",") )
comma = Literal(",").suppress()
comma = Suppress(",")
使用以上形式之一,我们解析出的结果变成这个样子:
['Hello'], ['World'], '!']
['Hi'], ['Mom'], '!']
['Good', 'morning'], ['Miss', 'Crabtree'], '!']
['Yo'], ['Adrian'], '!']
['Whattup'], ['G'], '?']
["How's", 'it', "goin'"], ['Dude'], '?']
['Hey'], ['Jude'], '!']
['Goodbye'], ['Mr.', 'Chips'], '!']
所以现在结果控制代码可以丢掉dummy变量了,只需:
for t in tests:
salutation, greetee, endpunc = greeting.parseString(t)
现在我们有了