9.8 使用XML转储和加载

    Python中的xml包中包含了很多用于解析XML文件的模块,也包括一个文件对象模型(Document Object Model,DOM)的实现,可用于生成XML文档。和之前的json模块一样,这并不是一个完整的对Python对象持久化的方案。然而,由于XML文件格式的普遍使用,Python对象与XML文档之间的转换经常会发生。

    XML文件的处理涉及到对象与XML结构之间的映射处理。在设计映射的时候需要很谨慎,需要考虑到XML格式存在的一些约束。由于在表达力强的对象与XML文档严格的层次性结构之间有很大区别,导致了映射复杂度的增加。

    一个XML属性或标签的内容是纯文本。当加载一个XML文档时,我们需要将这些值转换为应用中更有用的类型。在一些情况下,XML文档中可能会包括一些属性或标签,声明所期望的类型。

    如果要将这些限制都考虑进来,可以使用plistlib模块将一些Python中内置的结构转换为XML文档。我们会在第13章“配置文件和持久化”中对这个模块进行探究,可用它来加载配置文件。

    为了支持自定义类,json模块中提供了一些对JSON编码进行扩展的方法,plistlib 模块并没有提供这个功能。

    当考虑将一个Python对象转储为XML文档时,以下有3种常用的方式可用来创建文本。

    • 在类设计中包括XML输出方法。通过使用这种方法,我们的类生成的字符串就直接进入了XML文档中。
    • 使用xml.etree.ElementTree创建ElementTree节点并且返回这个结构。这可被表示为文本。
    • 使用一个外部的模板并将属性写入模板中。除非我们已经有了一个很复杂的模板工具,否则这种方法并不是很推荐。string.Template类在标准库中只适用于非常简单的对象。

    有时会需要在Python中创建通用的XML序列化器。创建一个通用的序列化器的问题在于,XML结构非常灵活。每个应用的XML都需要有唯一的XML模式定义(XML Schema Defination,XSD)或文档类型定义(Document Type Defination,DTD)。

    一个普遍的设计问题是如何对一个原子值进行编码。可以使用很多种方式达到目的。可以在标签的属性上使用一个可以标识类型的标签。另一个方式是将类型放在type标签里:<the_answer type="int">42</the_answer>。我们也可以使用嵌套的标签:<the_answer><int>42</int></the_answer>。或者,可以依赖于模式定义中的描述,建议the_answer应该为一个整数并且尽量不要编码为文本:<the_answer>42</the_answer>。也可以使用邻接的标签:<key>the_answer</key><int>42</int>。以上并不是所有的方案,XML还提供了很多其他的方式。

    当从XML文档中读取记录并创建Python对象时,我们被API的解析器限制了。一般地,我们需要对文档进行解析,然后检查XML标签的结构,最后使用有效数据创建Python对象。

    有一些Web框架,例如Django,包括了Django中定义的类的序列化操作。它与一般的Python对象的序列化是有区别的。序列化的定义由Django中的数据模型组件完成。另外,还定义了dexml、lxmlpyxser这些用于Python对象和XML之间绑定的包。可参见http://pythonhosted.org/dexml/api/dexml.htmlhttp://lxml.dehttp://coder.cl/products/pyxser/。这里还有一个更详细的列表:https://wiki.python.org/moin/PythonXml

    9.8.1 使用字符串模板转储对象

    将Python对象序列化为XML的一种方式是创建XML文本。这也是手动映射的一种,通常由一个映射函数来完成,它会生成Python对象所对应的XML。如果有一个复杂的对象,容器必须遍历其中的每一项。以下是对我们微博类结构的两种简单的扩展方式,添加了将XML输出为文本的功能。

    class Blog_X( Blog ):
      def xml( self ):
        children= "\n".join( c.xml() for c in self.entries )
        return """\
    <blog><title>{0.title}</title>
    <entries>
    {1}
    <entries></blog>""".format(self,children)
    class Post_X( Post ):
      def xml( self ):
        tags= "".join( "<tag>{0}</tag>".format(t) for t in self.tags )
        return """\
    <entry>
      <title>{0.title}</title>
      <date>{0.date}</date>
      <tags>{1}</tags>
      <text>{0.rst_text}</text>
    </entry>""".format(self,tags)

    以上XML输出方法的实现具有非常高的类的特殊性,它会输出XML中包含的相关属性。这种方式还不够一般化,Blog_X.xml()方法生成了一个<blog>标签,包含了一个标题和一些记录。Post_X.xml()方法输出了一个<post>标签,其中包含了一些属性。在这两种方法中,使用了"".join()"\n".join()来创建附属对象,后者基于短字符串元素创建一个长字符串。将一个Blog对象转换为XML可能如下所示。

    <blog><title>Travel</title>
    <entries>
    <entry>
      <title>Hard Aground</title>
      <date>2013-11-14 17:25:00</date>
      <tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#ICW</tag></
    tags>
      <text>Some embarrassing revelation. Including and □ </text>
    </entry>
    <entry>
      <title>Anchor Follies</title>
      <date>2013-11-18 15:30:00</date>
      <tags><tag>#RedRanger</tag><tag>#Whitby42</tag><tag>#Mistakes</
    tag></tags>
      <text>Some witty epigram.</text>
    </entry>
    <entries></blog>

    这种方式有两个缺陷。

    • 忽略了XML命名空间,还需稍微对文本进行改动来生成相应的标签。
    • 每个类中还需要适当地将<、&、>和‘’字符相应地转义为XML中的<、>、&和"。html模块中的html.escape()函数可以完成这类转换。

    这种方式可以生成XML,它虽能够有效执行但并不是很优雅而且不够通用。

    9.8.2 使用xml.etree.ElementTree转储对象

    我们可以使用xml.etree.ElementTree模块来创建Element结构,它用于生成XML。对它使用xml.domxml.minidom并不是很容易。DOM API需要拿到最上层的文档,然后创建每个元素。当对一个包含了一些属性的类进行序列化时,上下文对象的出现就显得有些复杂。我们需要先创建文档然后对文档中的所有元素序列化,将文档上下文作为一个参数传入。

    大体上来说,希望在设计的每个类中创建一个最上层元素并返回。最上层元素将包含一个子元素的序列,可以将文本和属性赋值给需要创建的每个元素,也可以将一个以结束标签为结尾的外部文本赋值为tail。对于内容模板而言,这只是空格。对于长的名称来说,以如下方式来导入ElementTree可能会方便些。

    import xml.etree.ElementTree as XML

    这里是两种对微博类结构的扩展实现,都是将XML输出的功能加入Element实例中,在Blog类中添加了如下方法。

    def xml( self ):
      blog= XML.Element( "blog" )
      title= XML.SubElement( blog, "title" )
      title.text= self.title
      title.tail= "\n"
      entities= XML.SubElement( blog, "entities" )
      entities.extend( c.xml() for c in self.entries )
      blog.tail= "\n"
      return blog

    Post类中添加了如下方法。

    def xml( self ):
      post= XML.Element( "entry" )
      title= XML.SubElement( post, "title" )
      title.text= self.title
      date= XML.SubElement( post, "date" )
      date.text= str(self.date)
      tags= XML.SubElement( post, "tags" )
      for t in self.tags:
        tag= XML.SubElement( tags, "tag" )
        tag.text= t
      text= XML.SubElement( post, "rst_text" )
      text.text= self.rst_text
      post.tail= "\n"
      return post

    以上XML输出的实现在类级别具有高度的抽象。它们将创建包含适当文本值的Element对象。

    在创建子元素的工作中,没有方便快速的方式。我们必须分别插入每一项对应的文本。

    blog方法中,可以使用Element.extend()来将每个文章记录放进<entry>元素中。这使得创建XML结构的工作灵活而简便。这一切都需要归功于XML命名空间。我们可以使用QName类来为XML命名空间定义适当的名称。ElementTree模块正确地对XML标签应用了命名空间筛选器。这种方式也正确地将<、&、>和''字符转换为XML中的&lt;&gt;&amp;和&quot;。这些方法中大部分输出的XML都会匹配上一节的内容,而空格会不同。

    9.8.3 加载XML文档

    从一个XML文档中加载Python对象分为两步。首先,我们需要对XML文本解析,用于创建文档对象。然后,需要对用于生成Python对象的文档对象进行检查。正如前面所介绍的,XML格式具有极大的灵活性,从XML到Python的序列化方法并不是唯一的。

    一种方式是遍历整个XML文档,使用类似XPath查询来对解析的元素进行定位。以下是一个遍历XML文档的函数,从XML中读取并生成BlogPost对象。

    import ast
    doc= XML.parse( io.StringIO(text.decode('utf-8')) )
    xml_blog= doc.getroot()
    blog= Blog( xml_blog.findtext('title') )
    for xml_post in xml_blog.findall('entries/entry'):
      tags= [t.text for t in xml_post.findall( 'tags/tag' )]
      post= Post(
        date= datetime.datetime.strptime(
          xml_post.findtext('date'), "%Y-%m-%d %H:%M:%S"),
        title=xml_post.findtext('title'),
        tags=tags,
        rst_text= xml_post.findtext('rst_text')
      )
      blog.append( post )
    render( blog )

    以上代码完成了对一个<blog>标签的遍历操作。它查找了<title>标签并获取了元素中的文本,用于创建最上层的Blog实例。然后它会查找<entries>元素内的<entry>子元素。这个过程将用于创建每个Post对象。Post对象中不同的属性会被分别转换。在<tags>元素中的每个<tag>元素的文本都会被转换为一个文本值列表。日期会从它的文本表示中解析出来。每个Post对象都会附加在全局的Blog对象上。这种从XML文本到Python对象的手动映射,在XML文档的整个解析过程中是常见的。