级别: 高级 David Mertz 博士 (mertz@gnosis.cx), 总裁, Gnosis Software, Inc.
2004 年 12 月 01 日 在很多方面 XML 绑定
gnosis.xml.objectify
被设计成更像是一个工具箱,而不是一种定型的工具。但这也(可能)让用户对如何专门化该工具来完成一般任务感到迷惑。本文中,David 将介绍如何只经过很简单的包装,就可以定制
gnosis.xml.objectify
,从而执行特定的活动,如通过 XPath 访问子对象、自动将对象重新序列化为 XML、改变访问节点的语法等。其中一些技术涉及针对已提供父类的相当琐碎的专门化工作,而另一些则涉及一些小的实用函数。
似乎每天都有新的 Python XML 绑定出现,不是因为现有的库(如
gnosis.xml.objectify
或
ElementTree
)中缺少了什么,只是因为这里未发明 综合症的表现。虽然可能有一定的偏见,但我仍然认为我的
gnosis.xml.objectify
—— 这类工具中开发出来第一个 —— 是最通用、最 Python 化的绑定工具(也是速度最快、对内存最友好的一个)。不幸的是,重复出现的目的相同、差别细微的库正是 Python 的苦恼之一,在其他几个领域也有这种情况。
开发人员发明自己的工具的部分原因,只不过是因为他们没有即刻发现如何用现有工具实现其目标。本文将在一定程度上对此加以补救,当然主要是针对
gnosis.xml.objectify
。
gnosis.xml.objectify 的设计思想
我创建
gnosis.xml.objectify
的目标是提供一个模块,完全将 XML 文档中的数据 转化成 Python 原生对象。具体地说,使用 getter 和 setter 或其他类似的方法来访问数据不太符合 Python 的习惯。在 Java 或其他某些语言中这样做很大程度上是由 Java 语言的风格造成的,风格就是在 DOM 甚至 Python 中做事情的方式。
对于
gnosis.xml.objectify
,所有来自 XML 文档的数据(不管是元素体还是 XML 属性)都是对象属性中的数据。如果给定的对象有多个同名的子对象,那么属性将指向具有类型名称的子对象列表。但即使对象只有一个具有给定名称的子对象,其行为也非常类似列表,以便进行迭代。在访问
gnosis.xml.objectify
对象时,可能做的最简单的事情就是几乎总是做 某些事情。
对于不熟悉这个库的读者而言,清单 1 提供了简单的快速入门知识和例子:
清单 1. gnosis.xml.objectify 的基本用法
>>> from gnosis.xml.objectify import make_instance
>>> xml = "<foo><bar>Text</bar><baz a1='bat'/><baz>blip</baz></foo>"
>>> foo = make_instance(xml)
>>> foo
<foo id="48b170">
>>> foo.bar
<bar id="48b300">
>>> foo.baz
[<baz id="48b210">, <baz id="48b030">]
>>> for bar in foo.bar: print bar
...
<bar id="48b300">
>>> foo.baz[0].a1
u'bat'
>>> foo.bar.PCDATA
u'Text'
>>> foo.bar[0].PCDATA
u'Text'
|
gnosis.xml.objectify 没有做什么
gnosis.xml.objectify
树中的节点对象被有意设计成没有什么智能。当然,它们能够以比较美观的格式进行打印,单个实例在适当的情况下类似列表,其他情况下则类似实例。但总的说来,节点对象避开了所有特殊方法或属性(至少在您为特定节点类型编写特殊的行为之前是这样,节点按照元素名称引用)。只有一个原因,会使添加到节点对象中所有名称都可能与
gnosis.xml.objectify
解析的一般 XML 文档中的标记名发生冲突。但更重要的是,我认为 Python 本身是一种非常好的语言(实际上非常棒),因此它可以而且应该对从 XML 源生成的对象使用那些在原有对象上工作很好的一般性技术。
但是我发现,
gnosis.xml.objectify
的灵活性给一些用户留下了不好的印象,而且这个发现有点晚了,它们不能实现一些更加面向 XML 的绑定,将这些绑定作为默认行为提供的受限制的目标。为了解决这个问题,我曾经为 Gnosis Utilities 包编写了一个子包,
gnosis.xml.objectify.utils
(请参阅参考资料),来说明询问最多的几种面向 XML 的用法。但是,这些工具虽然确实很有用,其目的更多的是作为例子来说明可以做什么,而不是
gnosis.xml.objectify
的正式 API。我的观点是,除了 Python 的 API 之外,
gnosis.xml.objectify
没有 自己的 API。
执行 XPath 搜索
人们看到 Fredrik Lundh 的
ElementTree
和 Uche Ogbuji 的
Anobind
(请参阅参考资料)的一个优点,即都使用了类 XPath 的节点搜索方法。据我看来,XPath 语法在某种程度仍然过于面向 XML,但是有很多用户要求这种功能,因此我在 Gnosis Utilities 中添加了一个工具函数 gnosis.xml.objectify.utils.XPath()。用了大约 50 行代码,我就实现了
ElementTree
或
anobind
所支持的 XPath 语法的一个重要超集,但还不是完整的 XPath 规范,那样做太庞大了。
具体地说,我启用了以下 XPath 特性:
- 通过制定标签名进行命名节点搜索
- 使用
// 分隔符进行递归搜索。
- 使用
* 符号进行通配符搜索。
- 使用
text() 伪函数进行文本节点搜索。
- 使用
@ 前缀进行属性搜索。
- 使用
@* 符号进行通配符属性搜索。
- 节点索引和分片。
此外,因为使用的是 Python,所以不仅允许用户使用简单的 XPath 数字索引,还允许用户使用一般的片段符号。因为 XPath 的索引从 1 开始,而 Python 以 0 为基数,所以要在伪 XPath 代码中以不同的方式表示片段来强调其非 XPath 语义,比如 /tagname[2..5] 表示文档中从第二到第五个 <tagname> 元素(包含两端)。
在这里,我将所有的处理编写成一个滞后迭代器,因此在不需要的时候,不必实例化大型的节点列表。当然,如果希望实例化节点列表,可以用 list(XPath(obj,path)) 实现它。
不过,虽然我认为预定的索引很酷,但这个简单的函数并没有费心去实现它。完整实现 XPath 的其他部分从概念上没有什么困难,对于示范而言,我还没有发现它的必要性(或者简洁性)。比方说,测试脚本 test_xpath.py 将包含在以后的 Gnosis Utilities 版本中,它包含了下面的 XPath 测试(和正确的输出):
清单 2. test_xpath.py 中的测试模式
patterns = '''/bar //bar //* /baz/*/bar
/bar[2] //bar[2..4]
//@a1 //bar/@a1 /baz/@* //@*
baz//bar/text() /baz/text()[3]'''
|
只有 4 行的节点遍历代码
为此我创建了一个小型的递归便利函数来遍历
gnosis.xml.objectify
对象中的所有节点。如果愿意您也可以直接使用这个函数。如果在树上执行自己的非 XPath 遍历,这个函数很有用。当然,这两个调用是等价的:walk_xo(obj) 和 XPath(o,"//*")(第一个执行的簿记工作更少一些)。该函数如下:
清单 3. 紧凑、滞后、递归的节点遍历
def walk_xo(o):
yield o
for node in children(o):
for child in walk_xo(node):
yield child
|
简单吧?另一个小型支持函数仅仅从(伪)XPath 表达式中解析出索引值,这里就不再重复了。
(基本)完整的 XPath 包装器
XPath() 函数如此简洁的诀窍在于根本 无需担心 XML(请参阅清单 4)。这里的大部分工作是理解 XPath 本身。某些已有的单行包装器函数,如 children()、text() 和 attributes(),让代码看起来很美观,但这只是极其简单的过滤器。换句话说,您可以对根本不是来自 XML 的对象使用与这个函数类似的一些代码。
清单 4. gnosis.xml.objectify.utils.XPath() 函数
def XPath(o, path):
"Find node(s) within an _XO_ object"
path = path.replace('//','/!!') # Placeholder hack for easy splitting
if path.startswith('/'): # No need for init / since node==root
path = path[1:]
if path.startswith('!!'): # Recursive path fragment
path, start, stop = indices(path)
i = 0
for node in walk_xo(o):
if i >= stop: return
for match in XPath(node, path[2:]):
if start <= i < stop:
yield match
i += 1
elif '/' in path[1:]: # Compound, non-recursive
head, tail = path.split('/', 1)
for node in XPath(o, head):
for match in XPath(node, tail):
yield match
else: # Atomic path fragment
path, start, stop = indices(path)
if path=="*": # Node wildcard
for node in islice(children(o), start, stop):
yield node
elif path=="text()": # Node text(s)
for s in islice(text(o), start, stop):
yield s
elif path.startswith('@*'): # All node attributes
for attr in attributes(o):
yield attr
elif path.startswith('@'): # Specific node attribute
for attr in attributes(o):
if attr[0]==path[1:]:
yield attr
elif hasattr(o, path): # Named node type
for node in islice(getattr(o, path), start, stop):
yield node
|

 |

|
序列化为 XML
用户经常受到这样的困扰,
gnosis.xml.objectify
没有把对象重新序列化为 XML。与其他 Python XML 绑定工具相比,这被说成是一个弱点。对此我不能苟同:其他那些绑定技术仍然强迫您按照 XML 而不是 Python 的习惯思考它们的 Python 对象。只有特定的对象和属性被序列化,而不是 Python 对象所有的 内容都被序列化。
比方说,在
ElementTree
中可以执行这样的步骤:
清单 5. ElementTree 例子
>>> from elementtree import ElementTree
>>> et = ElementTree.parse("xpath.xml")
>>> et.write(sys.stdout)
|
但是,如果改变对象 et(或者用 .getroot()、.find() 或 .findall() 之类方法生成的任何对象),那么添加的内容通常不能被序列化。比如,下面的代码虽然修改了对象,但并没有改变序列化的结果:
清单 6. 修改后 ElementTree 例子
>>> et.new = 'flaz'
>>> et.getroot().more = 123
>>> et.write(sys.stdout).
|
类似的,通过
Anobind
及其 .unbind() 方法,您可以使用 .append()、.insert() 或 .remove() 这样的 API 方法添加特定的面向 XML 的节点。但是,
gnosis.xml.objectify
也可以使用其 gnosis.xml.objectify.addChild() 实用函数添加特定的属性(使用 gnosis.xml.objectify.createPyObj() 得到将添加的特定 _XO_ 对象)。
如果只 需要一般的
gnosis.xml.objectify
序列化,比如原始 XML 中改变的几个值,只要 12 行代码就可以编写一个实用函数:
清单 7. 一般的 XML 序列化
def write_xml(o, out=stdout):
"Serialize an _XO_ object back into XML"
out.write("<%s" % tagname(o))
for attr in attributes(o):
out.write(' %s=%s' % attr)
out.write('>')
for node in content(o):
if type(node) in StringTypes:
out.write(node)
else:
write_xml(node, out=out)
out.write("</%s>" % tagname(o))
|
但按照我的观点,Python 中的对象真正的强大之处在于非一般性的序列化和转换。可能不仅仅是将每个属性转储到 XML 中,在写入之前,还要过滤和处理节点。当然,如何操作取决于应用程序的需要。
定制容器对象
Dave Kuhlman 的
generateDS
(请参阅参考资料)以及其他一些不够成熟的绑定技术,它们所采用的一种 XML 绑定方法要求对所处理文档中的每个 XML 元素类型提供一个定制的 Python 类。在 Kuhlman 的工具中,这些定制类从相应的 W3C XML Schema(但只允许完整 WXS 规范的一个子集)中生成。相反,
gnosis.xml.objectify
以及
ElementTree
、
Anobind
和其他一些工具,不需要任何特殊的编程就能绑定任何原有的 XML 文档。
但是,
gnosis.xml.objectify
类似于
Anobind
(不同于
ElementTree
),如果需要 使用允许您创建定制的节点类。事实上,您可以置换每种 节点对象的基类,从而能够定制整个应用程序的行为。
我认为,刚开始使用
gnosis.xml.objectify
的用户感受到了为每个标签名提供专门化类这种思想的压力。下面通过几个例子说明这种想法没有什么威胁。
重定义 _XO_ 基类
在定制基类时需要将新类添加到 gnosis.xml.objectify 名称空间中。这个步骤需要一点技巧,但也不是很难。也许将来我会在包装器函数中为这个步骤提供更加友好的名称,但采用这种风格是为了强调您正在改变模块本身。比方说,Gnosis Utilities 1.1.1 中的标签名发生了更改,而属性没有更改,这就增加了访问名称中包含 Python 变量禁用字符的属性的难度。一种解决方法是也允许对这些属性进行字典式的访问:
清单 8. 添加类似字典的属性访问
>>> import gnosis.xml.objectify
>>> class newXO(gnosis.xml.objectify._XO_):
... def __getitem__(self, key):
... return getattr(self,key)
...
>>> gnosis.xml.objectify._XO_ = newXO
>>> o = make_instance('<o><my-doc my-name="david">Stuff</my-doc></o>')
>>> print o.my__doc['my-name']
david
>>> getattr(o.my__doc,'my-name') # Works without custom base
u'david'
|
重定义基于标签名的节点类
对于确知其内容的基于标签名的特定类,重定义基类可能是最好的工具。比方说,如果特定文档中某个元素总是叶子节点(而且没有 XML 属性),您可能希望通过节点名本身来引用其 PCDATA。当然,如果输入的 XML 不符合假定的结构,这种情况下访问其孩子节点会更困难。对这种行为的一种编程方法如下:
清单 9. AutoPCData 定制节点类
>>> from gnosis.xml.objectify import make_instance
>>> xml = '''<group>
... <var><description>foo</description></var>
... <var><description>bar</description></var>
... </group>'''
group = make_instance(xml)
print group[0].variable[0].description
<description id="23cf2c">
print group[0].variable[0].description.PCDATA
foo
>>> import gnosis.xml.objectify
>>> class AutoPCDATA(gnosis.xml.objectify._XO_):
... def __repr__(self):
... return self.PCDATA
...
>>> gnosis.xml.objectify._XO_description = AutoPCDATA
>>> group = make_instance(xml)
>>> print group[0].variable[0].description
foo
|
如果要做得更好一点,AutoPCDATA 还可以检查对象的属性不是 .PCDATA,在不同的情况下返回不同的值。
另外一种依赖于应用程序的方法是定制需要执行计算式访问的类。Python 绑定技术中有一项技术称为 XMLObject,它给出的一个例子描述了包含多个成员的家庭:
清单 10. XML 形式的家庭树
<Family>
<Member Name="Abe" DOB="3/31/42" />
<Member Name="Betty" DOB="2/4/49" />
<Member Name="Edith" Father="Abe" Mother="Betty" DOB="8/30/80" />
<Member Name="Janet" Father="Frank" Mother="Edith" DOB="1/17/03" />
</Family>
|
不用管 XML 的层次结构、完全按名字访问家庭成员可能很方便。最明显的一种方法就是定制 Family 类:
清单 11. 用类字典的方法访问子属性
class Family(gnosis.xml.objectify._XO_):
def __getitem__(self, key):
for member in self.Member:
if member.Name = key:
return member
gnosis.xml.objectify._XO_Family = Family
Family = make_instance('family.xml')
print Family['Janet'].DOB
|
但是如果名字不是惟一的,可能还需要扩展这种特殊的方法。
结束语
文中介绍了包装
gnosis.xml.objectify
的一般技术,主要是抛砖引玉,帮助用户进行更加专门化的定制。保持 API 的高度开放性,减少专门化的成分,将定制的工作留到应用程序级上而是留在库这一级别上,这样做可以获得最大的灵活性和强大的功能。
参考资料
关于作者
对本文的评价
|