IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

XML 问题: 充分利用 gnosis.xml.objectify

利用实用函数增强对象的行为

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 高级

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 的高度开放性,减少专门化的成分,将定制的工作留到应用程序级上而是留在库这一级别上,这样做可以获得最大的灵活性和强大的功能。



参考资料



关于作者

对于 David Mertz 来说,整个世界就是一个舞台,而他的职责就是专门提供临场指导。您可以通过 mertz@gnosis.cx 与 David 联系,他的生活在 http://gnosis.cx/dW/ 上有翔实的记录。欢迎对本文、过去和将来的专栏提供意见和建议。请参阅 David 所著的 Text Processing in Python




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款