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、lxml和pyxser这些用于Python对象和XML之间绑定的包。可参见http://pythonhosted.org/dexml/api/dexml.html 、http://lxml.de和http://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.dom和xml.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中的<、>、&和"。这些方法中大部分输出的XML都会匹配上一节的内容,而空格会不同。
9.8.3 加载XML文档
从一个XML文档中加载Python对象分为两步。首先,我们需要对XML文本解析,用于创建文档对象。然后,需要对用于生成Python对象的文档对象进行检查。正如前面所介绍的,XML格式具有极大的灵活性,从XML到Python的序列化方法并不是唯一的。
一种方式是遍历整个XML文档,使用类似XPath查询来对解析的元素进行定位。以下是一个遍历XML文档的函数,从XML中读取并生成Blog和Post对象。
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文档的整个解析过程中是常见的。
