 | 级别: 初级 David Mertz, Ph.D. (mertz@gnosis.cx), 评估师, Gnosis Software, Inc.
2003 年 10 月 01 日 RXP 是一个用 C 编写的验证解析器,它创建XML文档的一个非 DOM 树表示。虽然 RXP 本身没有很好地提供说明文档,因为它并不是为怯懦的人准备的,但是至少有两个非常棒的高层 API 是在 RXP 之上构建的: pyRXP 和 LT XML。pyRXP 是 RXP 的一个 Python 绑定,而 LT XML 是一组实用程序和库。在本文中,David 介绍了 RXP,将它与 expat 解析器进行了对比,并简要讨论了 pyRXP 和 LT XML,这两者利用了 RXP 速度上的优势,但又避免了 RXP 的复杂性。
本专栏的读者将会感受到这个事实,虽然我是在这里对 XML 作一般性描述,但是我对于 Python 工具有特别的偏爱。我曾想在本期中打破以往这种模式,而注重于在
C 应用程序中使用
RXP
。不过在更深入地察看了
RXP
库后,我发现利用它的最简单的方法是通过 Python 模块
pyRXP
。
尽管底层
RXP
GPL 库几乎肯定是您能找到的最快的验证XML解析器,但是实际的解析器代码没有什么说明文档,只有一个简单的命令行工具示例。这个工具
rxp 类似于实用程序
xmlcat.py (我在技巧文章“
命令行 XML 处理”中已介绍过它)
以及各种类似的实用程序——它读取 XML 文档、对它们进行验证、并输出一种规范的形式。可以分析
rxp.c 文件的源代码,以了解
RXP
解析生成紧凑文档树作为数据结构的方法。
在
RXP
之上,语言技术组(Language Technology Group)又构建了
LT
XML
,它包含多种高层工具和 API。用
LT
XML
构建了其他一些工具,
包括 XED(一个XML编辑器)。在本文中,我将简要概述
LT
XML
中的工具,但是主要重点还是研究通过
pyRXP
绑定公开的
RXP
树 API。就我所知,其他本身就具有
RXP
绑定的高层语言——如Perl、TCL和Ruby——还没有加入它们。
谈论速度
RXP
是
快速的。一个使用(可选地)验证
RXP
解析器的 C 应用程序在速度上可能与使用非验证
expat
解析器(被认为是非常快的)没有什么差别。
RXP
通过为所要解析的XML 文档构建一个紧凑的内存树结构而工作。解析的失败就是构建树的失败,成功的解析给出一个比以 DOM 表示的 XML 文档资料有效得多的数据结构。
在需要从 XML 文档构建一个完整的数据结构时,
RXP
可能稍微胜过
expat
,而如果需要验证,那么肯定不会选用
expat
。不过,如果只是顺序处理,或者提取 XML 文档中的一小部分信息,那么
expat
会更好一些,因为它不需要保存任何已经处理的
(或者已经跳过的)标记的表示。事实上,对于十分大的文档,
expat
具有压倒性的优势——很少需要创建一个 1 GB XML 文档的内存表示(而使用
RXP
则
毫无选择地需要创建这样的内存表示)。一个使用
expat
构建的应用程序在读取这么大的 XML 时可以只提取少部分感兴趣的标记,使用的命令占用的内存比文档大小小得多。
RXP
的速度在
pyRXP
绑定的上下文中可以真正体现出来。本文的最后部分
Python中几个XML文档模型的比较 对这些模型:
ElementTree
、
gnosis.xml.objectify、
xml.minidom
和
cDomlette
进行了一些非常详细的速度和内存使用
的比较。测试就是用第一种 API 创建一个最小的内存表示,并测量这种构建所用的时间和所需要的内存使用。用
pyRXP
做同样的事情更容易:
清单 1. time_rxp.py
from pyRXP
import Parser
import sys, time
start = time.clock()
tups = Parser().parse(sys.stdin.read())
print
"Time: %.3f" % (time.clock()-start)
|
用
pyRXP
解析 3MB 的
weblog.xml 文件只
需用 4 秒钟,在以前的测试中最好的性能是
cDomlette
,它在我的测试计算机上用了大约 25 秒。在内存使用方面,
time_rxp.py 最多时使用大约 28
MB,与以前的竞争者中最节俭的一个
gnosis.xml.objectify
相似。换句话说,
pyRXP
具有最好的内存使用,并且速度是以前最快的 6 倍!
pyRXP
比其他 XML 文档模型 API 快得多有一个非常特别的原因:
RXP
用 C 构建一个完整的数据结构,而
pyRXP
所需要做的只不过是将这个完整的结构转换为一个非常类似的 Python 数据结构。相反,像
gnosis.xml.objectify
和
ElementTree
这样的模块——虽然利用底层
expat
解析器进行实际的解析——仍然需要对每一个标记或者遇到的一部分内容在 Python 函数中进行回调。在 Python 中函数调用的开销是很大的,特别是与 C 调用的廉价相比。原则上,可以编写一个基于
expat
的、用 C 编码的 Python 扩展,它构建一个完整的数据结构,然后将它送回 Python
解释器(其速度将类似于
pyRXP
)。但是创建一个扩展将需要比
pyRXP
包装器更多的编程工作,因为即使在 C
expat
中,也需要对每一个标记和内容编写回调。相反,
RXP
就在解析器中建立数据结构。
pyRXP 的元组树数据结构
pyRXP
(以及
RXP
本身
)使用一个高效的、轻量级的树表示 XML 文档。树中的每一个节点是 form 的一个元组:
(tagname, attr_dict, child_list, reserved) |
表示中没有使用专门的 Python 类——只使用了元组、字典、列表和字符串(以及保留位置上的
None )。也许令人意外的是,这个 form 足以表示 XML 文档中的所有信息。tagname 是直观的字符串
;正如您所想到的,属性字典是一个映射属性与值的字典。子列表更为精巧:在列表中字符串可以与元组交错,表示混合的内容元素。而一个没有内容的元素由一个空的子列表表示,但是一个自封闭的标记由
None 表示
。 清单 2 显示了这种结构:
清单 2. pyRXP 元组树数据结构
>>> import pprint
>>> xml = '''<foo this="that" spam="eggs">
... <bar>1</bar><bar>2</bar>
... <baz></baz><baz/></foo>'''
>>> tree = Parser().parse(xml)
>>> pprint.pprint(tree)
('foo',
{'this': 'that', 'spam': 'eggs'},
['\\n',
('bar', None, ['1'], None),
('bar', None, ['2'], None),
'\\n',
('baz', None, [], None),
('baz', None, None, None)],
None)
|
所有 XML 信息都在这里,但是浏览它可能不方便。
比较数据访问样式
回想一下,在上一期中,我比较了一个简单应用程序的几种实现,该应用程序筛选一个测试 weblog.xml 文档、并显示其中的一些信息。这个文件中的一个
<entry> 元素看起来可能像下面这样:
清单 3. 一个 weblog.xml 项记录
<entry>
<host>64.172.22.154</host>
<referer>-</referer>
<userAgent>-</userAgent>
<dateTime>19/Aug/2001:01:46:01</dateTime>
<reqID>-0500</reqID>
<reqType>GET</reqType>
<resource>/</resource>
<protocol>HTTP/1.1</protocol>
<statusCode>200</statusCode>
<byteCount>2131</byteCount>
</entry>
|
文件 weblog.xml 包含上千个这种项。一个使用
gnosis.xml.objectify
的筛选器看起来像下面这样:
清单 4. select_hits_xo.py
from gnosis.xml.objectify
import XML_Objectify, EXPAT
weblog = XML_Objectify(
'weblog.xml',EXPAT).make_instance()
interesting = [entry
for entry
in weblog.entry
if entry.host.PCDATA==
'209.202.148.31'
and entry.statusCode.PCDATA==
'200']
for e
in interesting:
print
"%s (%s)" % (e.resource.PCDATA, e.byteCount.PCDATA)
|
如何对一个
pyRXP
元组树编写同样的应用程序呢?不幸的是,由于必须在嵌套的列表和数字元组位置中查找,
所以访问没有那么直观:
清单 5. select_hits_rxp1.py
from pyRXP
import Parser
TAGNAME,ATTRS,CHILDREN = range(
3)
weblog = Parser().parse(open(
'weblog.xml').read())
interesting = []
for child
in weblog[CHILDREN]:
if child[TAGNAME]!=
'entry':
continue
gotHost, gotStatus =
0,
0
for fld
in child[CHILDREN]:
tag = fld[TAGNAME]
if tag==
'host'
and fld[CHILDREN]==[
'209.202.148.31']:
gotHost =
1
elif tag==
'statusCode'
and fld[CHILDREN]==[
'200']:
gotStatus =
1
if gotHost
and gotStatus:
interesting.append(child[CHILDREN])
for e
in interesting:
resource, byteCount =
'',
''
for fld
in e:
if fld[TAGNAME]==
'resource':
resource = fld[CHILDREN][
0]
elif fld[TAGNAME]==
'byteCount':
byteCount = fld[CHILDREN][
0]
print
"%s (%s)" % (resource, byteCount)
|
即使用一些指定的常量代替元组位置,这个版本也无疑是难于阅读的(但是我认为对于直接处理元组树这已经是最好的了)。输出是一样的,只不过
pyRXP
的版本在 5 秒钟
内而不是 25 秒就得到了这个输出。
pyRXP
模块与一些其他文件混杂在一起,其中一个有意思的模块称为
xmlutils
。在一种聪明的策略中,类
xmlutils.TagWrapper
充当
pyRXP
元组树
的代理包装器。整体效果是可以以非常类似于
gnosis.xml.objectify
或者
ElementTree
所提供的自然
的 Python 样式访问元组树:
清单 6. select_hits_rxp2.py
from pyRXP
import Parser
import xmlutils
tree = Parser().parse(open(
'weblog.xml').read())
weblog = xmlutils.TagWrapper(tree)
interesting = [child
for child
in weblog
if child.tagName==
'entry'
if str(child.host)==
'209.202.148.31'
if str(child.statusCode)==
'200']
for e
in interesting:
print
"%s (%s)" % (e.resource, e.byteCount)
|
至目前为止都不错。代码相当优雅。不过,代理增加了一些开销。这个版本的脚本运行了 7.5 秒而不是 5 秒,这比
gnosis.xml.objectify
的25 秒还是好得多。不过,筛选器在代理开销上所花费的这 2.5 秒相当于
select_hits_xo.py 在其筛选
过程中不用花费十分之一秒。分析步骤掩盖了这种差别,但是如果想像一个对XML文档进行一次解析、然后执行数百个不同筛选行动(例如对用户规则)的应用程序,那么代理包装器就开始变得不那么有吸引力了。不过,
pyRXP
开发人员警告说
xmlutils
是试验性的,所以也许可以开发出更有效的包装器。
使用 LT XML
LT
XML
集合建立在
RXP
之上,并包含多种处理 XML 的命令行工具,以及一些比
RXP
本身的 API 更高层的 API。
LT
XML
中一个强大的工具称为
sggrep ,它是 XML 文件的一种
grep。 它的语法有些令人困惑,但是基本上它是将结合了常规表达式和 XPath 的表达式进行公式化的方法。
LT
XML
中的一些其他工具包括:
-
textonly ,它去掉标记并输出 PCDATA 内容。
-
sgsort 用于 XML 元素排序。
-
sgcount 用于元素统计。
-
xmlnorm 用于规范化 XML 文档。
其中每一种工具都使用输入和输出管道,因而可以在命令行和 shell 脚本中结合。而且,将“sg”前缀从许多名字中去掉就可以看到与非 XML 版本的类似工具的关系。
一种有意思的技术是将几个
sggrep 查询在一个管道中传送。每一个
sggrep 命令可以同时指定主查询和子查询。例如:“我要
<foo>
元素,其中包含内容为
baz 的
<bar> 元素
”。主查询要求
<foo>, 子查询指定子元素
<bar> 的属性。工具允许用
-q 、
-s 和
-t 显式指定查询、子查询和样式的更详细的形式,也可以使用不带开关的紧凑形式
(用
--
开关启用紧凑形式)。清单 7 是一个复杂的命令行,它与上面讨论的筛选工具所完成的工作几乎一样:
清单 7. 一个 webhost.xml 筛选复合查询
% cat weblog.xml |
sggrep '.*/entry' '.*/entry/host' '209.202.148.31' -- |
sggrep -q '.*/entry' -s '.*/entry/statusCode' -t '200' |
sggrep '.*/resource|byteCount' -- |
textonly -s '\\n'
|
其输出并不很正确,它分成了几行,如下所示:
/publish/programming/regular_expressions.html
45674
|
而不是像 Python 筛选器那样按行编排格式:
/publish/programming/regular_expressions.html (45674)
|
也许可以聪明地使用一些标准的 Unix shell 工具,如
awk 、
sed 或者
tr, 以准确得到所需要的输出。
在好的方面,
sggrep 和其他
LT
XML
工具是相当快的,与不使用
TagWrapper 开销的
pyRXP
一样快。而且,由绑定的工具所带来的潜在能力也可以被希望使用类似 API 的 C 程序员所利用。也许最好的是,
LT
XML
本身现在有了一个 Python 绑定(但是有意思的是,其他脚本语言没有)。
参考资料
关于作者
对本文的评价
|  |