Pyparsing中文教程V2

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Pyparsing 导引
by Pau McGuire
你需要从文本文件或网页中提取数据吗?或者你想要更人性化的用户命令接口或者搜索字符串?正则表达式和lex/yacc让你的眼睛和脑袋疼?
Pyparsing将成为解决方案。

Pyparsing是纯python的类库,其能让你容易的建立递归下降(recursive-descent)解析器。

这不需要你自己写个解析器。

只要使用pyparsinng,你能够解析HTML,轻松建立日志文档数据提取器,复杂数据结构以及命令解释器。

这个Short Cut将告诉你怎么做!
Pyparsing 导引
Pyparsing是什么?
Pyparsing程序的简单形式
从Pyparsing出引用名字
定义语法
使用语法解析输入文本
处理解析后的文本
“Hello,World”在Steroids
什么使得Pyparsing变得不同?
类名比特殊符号好读并且好理解
语法定义中的空格的扰乱
结果应当比单纯的列表形式多更多东西
在解析时间就执行的预处理
语法必须对改变具有更强的适应性和健壮性
从表格文件中解析数据-使用解析行为和ParseResults
从网页数据中提取数据
一个简易S表达式解析器
一个完整的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目录下,或者被你的应用包含。

慷慨的许可证-MIT许可证允许任意商用或非商用。

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,[http://
]的文章”用Python建立递归下降解析器”(http://
/-pub/a/python/2006/01/26/pyparsing.html).在这一节,我也使用类似的例子以介绍简单的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)
现在我们有了一个不错的解析器并可以处理它的返回结果。

让我们开始快乐的处理测试数据,首先,让我们将问候以及问候对象加进它们的表里:
salutes = []
greetees = []
for t in tests:
salutation, greetee, endpunc = greeting.parseString(t)
salutes.append( ( " ".join(salutation), endpunc) )
greetees.append( " ".join(greetee) )
我们还有其他一些小变化:
使用” “.join(list)去将拆出来的标记列表转回简单的字符串
保存结尾的符号与问候语组成元
现在我们收集一些名字和问候语,我们可以使用它们设计一些额外的,之前没见过的句子:
for i in range(50):
salute = random.choice( salutes )
greetee = random.choice( greetees )
print "%s, %s%s" % ( salute[0], greetee, salute[1] )
现在我们可以看到全新的问候了:
Hello, Miss Crabtree!
How's it goin', G?
Yo, Mr. Chips!
Whattup, World?
Good morning, Mr. Chips!
Goodbye, Jude!
Good morning, Miss Crabtree!
Hello, G!
Hey, Dude!
How's it goin', World?
Good morning, Mom!
How's it goin', Adrian?
Yo, G!
Hey, Adrian!
Hi, Mom!
Hello, Mr. Chips!
Hey, G!
Whattup, Mr. Chips?
Whattup, Miss Crabtree?
...
我们也可以模拟一些介绍通过以下代码:
for i in range(50):
print '%s, say "%s" to %s.' % ( random.choice( greetees ),"".join( random.choice( salutes ) ),random.choice( greetees ) )
看起来像这样!
Jude, say "Good morning!" to Mom.
G, say "Yo!" to Miss Crabtree.
Jude, say "Goodbye!" to World.
Adrian, say "Whattup?" to World.
Mom, say "Hello!" to Dude.
Mr. Chips, say "Good morning!" to Miss Crabtree.
Miss Crabtree, say "Hi!" to Adrian.
Adrian, say "Hey!" to Mr. Chips.
Mr. Chips, say "How's it goin'?" to Mom.
G, say "Whattup?" to Mom.
Dude, say "Hello!" to World.
Miss Crabtree, say "Goodbye!" to Miss Crabtree.
Dude, say "Hi!" to Mr. Chips.
G, say "Yo!" to Mr. Chips.
World, say "Hey!" to Mr. Chips.
G, say "Hey!" to Adrian.
Adrian, say "Good morning!" to G.
Adrian, say "Hello!" to Mom.
World, say "Good morning!" to Miss Crabtree.
Miss Crabtree, say "Yo!" to G.
...
好了,我们已经见识了pyparsing模块。

通过使用一极其简单的pyparsing类和方法,我们准备向世界高喊”怎么样”
什么使得Pyparsing变得不同?
Pyparsing被设计为满足一些特别的目标,其中包括语法必须易写易理解而且能够很容易修改一个解析器去适应新的需求。

这些目标在于极简化解析器设计任务使pyparsing用户能聚焦于解析而不是在解析库与元语法之间挣扎,下面是pyparsing 之禅.
语法规则的编写应是自然易读的python程序,而且其形式为python程序员所熟悉。

Pyparsing以以下方式实现该目标
使用操作符组合解析器要素。

python支持操作符重载,利用它我们可以超越常规的对象式语法结构,使得我们的解析器表达式更易读。

比如说:
streetAddress = And( [streetNumber, name,Or(
[Literal("Rd."), Literal("St.")] ) ] )
可以被写成
streetAddress = streetNumber + name + ( Literal("Rd.") |
Literal("St.") )
很多pyparsing的属性设置方法会返回调用对象本身,所以一些属性调用可以组成一个链。

作为例子,下面的解析器定义了interger,并为其指定了名字和解析动作,解析动作将字符串转回python整数。

使用上述特性,可以将
integer = Word(nums)
= "integer"
integer.ParseAction = lambda t: int(t[0])
写为
integer =
Word(nums).setName("integer").setParseAction(lambda
t:int(t[0]))
类名比特殊符号好读并且好理解
这可能是pyparsing与正则表达式或类似的工具之间最明显的区别了。

简介处的IP 地址-电话号码例子就显示了这一点。

但正则表达式在它自己的控制符号出现在它想模式匹配的文本中时,会陷入理解上的真正困境。

结果我们会写出一个转义斜
杠的大杂烩。

这个理有一个正则表达式试图匹配C式函数,其可以有一个或多个参数,其由单词或整数构成:
(\w+)\((((\d+|\w+)(,(\d+|\w+))*)?)\)
括号是控制字符还是要匹配的字符并不是一目了然的,而如果输入文本包括,.,*或?情况将变得更糟糕。

而pyparsing版本描述类似的表达式则为:
Word(alphas)+ "(" + Group( Optional(Word(nums)|Word(alphas) + ZeroOrMore("," + Word(nums)|Word(alphas))) ) + ")"
这当然更好读。

由于x + ZeroOrMore(“,”+x)的形式过于普遍,我们有一个pyparsing 的辅助方法,delimitedList方法,它等价于这个表达式。

通过使用delimitedList,我们的pyparsing写法可以更简化为:
Word(alphas)+ "(" + Group(
Optional(delimitedList(Word(nums)|Word(alphas))) ) + ")"
语法定义中的空格的扰乱
对于”特殊符号不特殊”问题,正则表达式必须明确声明空格在输入文本的位置。

在C函数例子中,正则表达式应当匹配到
abc(1,2,def,5)
而不是
abc(1, 2, def, 5)
不幸的是,宣布可选空格出现或不出现并不容易,其结果是\s*的表达形式从头到尾到处都是,使我们所要匹配的目标更加模糊:
(\w+)\s*\(\s*(((\d+|\w+)(\s*,\s*(\d+|\w+))*)?)\s*\)
对应的,pyparsing无视两个要素之间的空格,所以对应的表达式还是:
Word(alphas)+ "(" + Group(
Optional(delimitedList(Word(nums)|Word(alphas))) ) + ")"
并不需要对空格多说什么。

类似的概念也可以应用到注释上,其可能会出现在代码的任何地方(不是那种只能在任一行最后的那种)。

想象注释像空格一样出现在参数之间,正则表达式会变成什么样子,不过在pyparsing中,只需要这些代码就可以解决。

cFunction = Word(alphas)+ "(" + Group(
Optional(delimitedList(Word(nums)|Word(alphas))) ) + ")"
cFunction.ignore( cStyleComment )
结果应当比单纯的列表形式多更多东西
Pyparsing的解析结果是ParseResult类的实例。

其可以被当成列表操作(使用[],len,iter或分片等)。

但它也可以当成嵌套(nested)结果,字典风格的以及对象风格(字段调用风格)。

C函数例子中的解析结果一般看作:
['abc', '(', ['1', '2', 'def', '5'], ')']
你可以看到参数被放进了一个子列表中,这使得进一步解析更容易。

而如果语法定义中给结果中的某些结构命了名,则我们可以使用字段引用法取代索引值法来减少出错概率。

这些高级引用计数在处理更复杂的语法问题时是至关重要的。

在解析时间就执行的预处理
当解析时,解析器对输入文本进行很多检查:测试不同的字符串或匹配一些模式,如两个引号间的字符串。

如果匹配到一个字符串,则发送解析(post-parsing)代码可发动一个转换,将其转为python整数类型或字符串类型对象。

pyparsing支持在解析时间调用的回调函数(成为解析行为)你可以附加一个单独的表达式给语法。

解析器会在匹配到对应形式时调用这些函数。

作为例子,从文本中提取双引号围成的字符串,一个简单地解析行为会移除掉引号。

像这样:
quotedString.setParseAction( lambda t: t[0][1:?1] )
就够了。

不需要检查开头结尾是不是引号-这个函数只有匹配到这样的形式时才会调用。

解析行为也可以用来执行不同的检查,像测试匹配到的词是不是一个给定列表中的词,并在不是时抛出一个ParseException。

解析行为也可以返回一个列表或一个对象,如将输入文本编译为一系列可执行或能调用的对象。

解析行为在pyparsing 中是个有力的工具。

语法必须对改变具有更强的适应性和健壮性
解析器中最普遍的死亡陷进在你编写时很难躲开。

一个简单的模式匹配执行器可能日渐变得更复杂和笨拙,输入文本的数据不能被匹配却被需要,于是解析器需要加上一个补丁以解决这一新变化。

或者去修改它的语言规则。

在这一切发生后一段时间,补丁开始干扰早先的语法定义,而之后越来越多的补丁使问题变得越来越困难。

当一个修改在你最后一次修改几个月发生时,你去重新理解它花去的时间超出你的想象,这都增加了困难。

pyparsing也没有解决这一问题,但是其单独定义的语法风格和结构使问题得以缓解-单独的要素容易找到也易读,易于修改和扩展。

这是pyparsing的使用者们发给我的一句话:“我可以只写一个自定义方法,但是我过去的经验反应一旦我创建一个pyparsing语法,它就会自动变得有组织而且容易维护和扩展。


从表格文件中解析数据-使用解析行为和ParseResults
作为我们第一个例子,让我们看一个处理给定橄榄球比赛文件信息文件的程序。

该文件每一行记录一场比赛的信息,有时间,比赛双方和他们的比分。

09/04/2004 Virginia 44 Temple 14
09/04/2004 LSU 22 Oregon State 21
09/09/2004 Troy State 24 Missouri 14
01/02/2003 Florida State 103 University of Miami 2
这些数据的BNF形式是简洁明了的
digit ::= '0'..'9'
alpha ::= 'A'..'Z' 'a'..'z'
date ::= digit+ '/' digit+ '/' digit+
schoolName ::= ( alpha+ )+
score ::= digit+
schoolAndScore ::= schoolName score
gameResult ::= date schoolAndScore schoolAndScore
我们以覆盖BNF定义来开始我们的解析器构建工作。

就像扩展”Hello,World!”程序一样,我们将先设计好构建块,然后再将它们组合起来构成更复杂的语法。

# nums and alphas are already defined by pyparsing
num = Word(nums)
date = num + "/" + num + "/" + num
schoolName = OneOrMore( Word(alphas) )
注意我们可以使用+操作符或组合pyparsing表达式和字符串符号(literals)。

通过组合这些加单的元素为更大的表达式,我们可以完成语法定义。

score = Word(nums)
schoolAndScore = schoolName + score
gameResult = date + schoolAndScore + schoolAndScore
我们使用gameResult对象去解析输入文本的每一行:
tests = """\
09/04/2004 Virginia 44 Temple 14
09/04/2004 LSU 22 Oregon State 21
09/09/2004 Troy State 24 Missouri 14
01/02/2003 Florida State 103 University of Miami
2""".splitlines()
for test in tests:
stats = gameResult.parseString(test)
print stats.asList()
就像我们曾在”Hello,World”解析器里看的那样,我们从这个语法中得到一个没有结构性的表。

['09', '/', '04', '/', '2004', 'Virginia', '44', 'Temple',
'14']
['09', '/', '04', '/', '2004', 'LSU', '22', 'Oregon',
'State', '21']
['09', '/', '09', '/', '2004', 'Troy', 'State', '24',
'Missouri', '14']
['01', '/', '02', '/', '2003', 'Florida', 'State', '103',
'University', 'of',
'Miami', '2']
第一个我们将进行的改进是将返回的有关日期的分散的数据组合成简单的MM/DD/YYYY型字符串。

我们只要用Combine类将表达式包起来就行了。

date = Combine( num + "/" + num + "/" + num )
解析结果变为
['09/04/2004', 'Virginia', '44', 'Temple', '14']
['09/04/2004', 'LSU', '22', 'Oregon', 'State', '21']
['09/09/2004', 'Troy', 'State', '24', 'Missouri', '14']
['01/02/2003', 'Florida', 'State', '103', 'University', 'of', 'Miami', '2']
Combine实际上为我们做了两件事。

第一是将匹配的标签合并进一个字符串,而它还使这些文本紧临在一起。

下一个改进是将学校名字组合起来。

因为Combine的默认行为要求标记相邻,所以我们将不使用它。

取代的,我们定义一个在解析时运行的过程,组合和返回单个字符串的标记.向前面提到的那样,这类过程通过解析行为实现,它们在解析过程时执行一些函数。

对这个例子,我们将定义一个解析行为,其接受被解析的标记,使用字符串的join 函数,返回组合后的字符串,这个加单的解析动作被一个python的lambda表达式描绘。

这个解析行为与lambda表达式的绑定是通过调用一个叫setParseAction的函数,像这样:
schoolName.setParseAction( lambda tokens: " ".join(tokens) )
这类手法的另一个用法是用于进行超越表达式定义的语法匹配的额外语义确认。

作为例子,之前的data的表达式会接受像03023/808098/29921这样的字符串作为有意义的数据,而这显然不是我们所期望的。

一个解析行为的对输入日期的赋意义化可以通过使用time.strptime方法去分析时间字符串。

time.strptime(tokens[0],"%m/%d/%Y")
如果strptime检定失败,则它会抛出一个ValueError异常。

Pyparsing使用它独特的异常类,PrseException,去作为表达式匹配与否的信号。

解析行为可以抛出它们独有的异常去标示,哪怕语法判定通过,但却由一些高级的语义判定所触发。

我们的解析行为将看起来像这样:
def validateDateString(tokens):
try:
time.strptime(tokens[0], "%m/%d/%Y")
except ValueError,ve:
raise ParseException("Invalid date string (%s)" %
tokens[0])。

相关文档
最新文档