Python3标准库:xml.etree.ElementTree XML操纵API

1. xml.etree.ElementTree XML操纵API

ElementTree库提供了一些工具,可以使用基于事件和基于文档的API来解析XML,可以用XPath表达式搜索已解析的文档,还可以创建新文档或修改现有文档。

1.1 解析XML文档

已解析的XML文档在内存中由ElementTree和Element对象表示,这些对象基于XML文档中节点嵌套的方式按树结构互相连接。

用parse()解析一个完整的文档时,会返回一个ElementTree实例。这个树了解输入文档中的所有数据,另外可以原地搜索或操纵树中的节点。基于这种灵活性,可以更方便的处理已解析的文档,不过,与基于事件的解析方法相比,这种方法往往需要更多的内存,因为必须一次加载整个文档。

对于简单的小文档(如下面的播客列表,被表示为一个OPML大纲),内存需求不大。

podcasts.opml:

<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
    <title>My Podcasts</title>
    <dateCreated>Sat, 06 Aug 2016 15:53:26 GMT</dateCreated>
    <dateModified>Sat, 06 Aug 2016 15:53:26 GMT</dateModified>
</head>
<body>
  <outline text="Non-tech">
    <outline
        text="99% Invisible" type="rss"
        xmlUrl="http://feeds.99percentinvisible.org/99percentinvisible"
        htmlUrl="http://99percentinvisible.org" />
  </outline>
  <outline text="Python">
    <outline
        text="Talk Python to Me" type="rss"
        xmlUrl="https://talkpython.fm/episodes/rss"
        htmlUrl="https://talkpython.fm" />
    <outline
        text="Podcast.__init__" type="rss"
        xmlUrl="http://podcastinit.podbean.com/feed/"
        htmlUrl="http://podcastinit.com" />
  </outline>
</body>
</opml>

要解析这个文档,需要向parse()传递一个打开的文件句柄。

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

print(tree)

这个方法会读取数据、解析XML,并返回一个ElementTree对象。

1.2 遍历解析树

要按顺序访问所有子节点,可以使用iter()创建一个生成器,该生成器迭代处理这个ElementTree实例。

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.iter():
    print(node.tag)

这个例子会打印整个树,一次打印一个标记。

如果只是打印播客的名字组和提要URL,则可以只迭代处理outline节点(而不考虑首部中的所有数据),并且通过查找attrib字典中的值来打印text和xmlUrl属性。

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.iter('outline'):
    name = node.attrib.get('text')
    url = node.attrib.get('xmlUrl')
    if name and url:
        print('  %s' % name)
        print('    %s' % url)
    else:
        print(name)

iter()的'outline'参数意味着只处理标记为'outline'的节点。

1.3 查找文档中的节点 

查看整个树并搜索有关的节点可能很容易出错。前面的例子必须查看每一个outline节点,来确定这是一个组(只有一个text属性的节点)还是一个播客(包含text和xmlUrl的节点)。要生成一个简单的播客提要URL列表而不包含名字或组,可以简化逻辑,使用findall()来查找有更多描述性搜索特性的节点。

对以上第一个版本做出第一次修改,用一个XPath参数来查找所有outline节点。

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.findall('.//outline'):
    url = node.attrib.get('xmlUrl')
    if url:
        print(url)

这个版本中的逻辑与使用getiterator()的版本并没有显著区别。这里仍然必须检查是否存在URL,只不过如果没有发现URL,它不会打印组名。

outline节点只有两层嵌套,可以利用这一点,把搜索路径修改为.//outline/outline,这意味着循环只处理outline节点的第二层。

from xml.etree import ElementTree

with open('podcasts.opml', 'rt') as f:
    tree = ElementTree.parse(f)

for node in tree.findall('.//outline/outline'):
    url = node.attrib.get('xmlUrl')
    print(url)

输入中所有嵌套深度为两层的outline节点都认为有一个xmlURL属性指向播客提要,所以循环在使用这个属性之前可以不做检查。

不过,这个版本仅限于当前的这个结构,所以如果outline节点重新组织为一个更深的树,那么这个版本就无法正常工作了。

1.4 解析节点属性

findall()和iter()返回的元素是Element对象,各个对象分别表示XML解析树中的一个节点。每个Element都有一些属性可以用来获取XML中的数据。可以用一个稍有些牵强的示例输入文件data.xml来说明这种行为。 

<?xml version="1.0" encoding="UTF-8"?>
<top>
    <child>Regular text.</child>
    <child_with_tail>Regular text.</child_with_tail>"Tail" text.
    <with_attributes name="value" foo="bar"/>
    <entity_expansion attribute="This &#38; That">
        That &#38; This
    </entity_expansion>
</top>

可以由attrib属性得到节点的XML属性,attrib属性就像是一个字典。

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

node = tree.find('./with_attributes')
print(node.tag)
for name,value in sorted(node.attrib.items()):
    print(name,value)

输入文件第5行上的节点有两个属性name和foo。

还可以得到节点的文本内容,以及结束标记后面的tail文本。

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

for path in ['./child','./child_with_tail']:
    node = tree.find(path)
    print(node.tag)
    print('child node text:',node.text)
    print('and tail text:',node.tail)

第3行上的child节点包含嵌入文本,第4行的节点包含带tail的文本(包括空白符)。

返回值之前,文档中嵌入的XML实体引用会被转换为适当的字符。

from xml.etree import ElementTree

with open('data.xml', 'rt') as f:
    tree = ElementTree.parse(f)

node = tree.find('entity_expansion')
print(node.tag)
print('in attribute:',node.attrib['attribute'])
print('in text:',node.text.strip())

这个自动转换意味着可以忽略XML文档中表示某些字符的实现细节。

1.5 解析时监视事件

另一个处理XML文档的API是基于事件的。解析器为开始标记生成start事件,为结束标记生成end事件。解析阶段中可以通过迭代处理事件流从文档抽取数据,如果以后没有必要处理整个文档,或者没有必要将解析文档都保存在内存中,那么基于事件的API就会很方便。

有以下事件类型:

start遇到一个新标记。会处理标记的结束尖括号,但不处理内容。

end已经处理结束标记的结束尖括号。所有子节点都已经处理。

start-ns结束一个命名空间声明。

end-ns结束一个命名空间声明。

iterparse()返回一个iterable,它会生成元组,其中包含事件名和触发事件的节点。 

from xml.etree.ElementTree import iterparse

depth = 0
prefix_width = 8
prefix_dots = '.' * prefix_width
line_template = '.'.join([
    '{prefix:<0.{prefix_len}}',
    '{event:<8}',
    '{suffix:<{suffix_len}}',
    '{node.tag:<12}',
    '{node_id}',
])
EVENT_NAMES = ['start','end','start-ns','end-ns']
for (event,node) in iterparse('podcasts.opml',EVENT_NAMES):
    if event == 'end':
        depth -= 1

    prefix_len = depth * 2

    print(line_template.format(
        prefix = prefix_dots,
        prefix_len = prefix_len,
        suffix = '',
        suffix_len = (prefix_width - prefix_len),
        node = node,
        node_id = id(node),
        event = event,
    ))

    if event == 'start':
        depth += 1

默认的,只会生成end事件。要查看其他事件,可以将所需的事件名列表传入iterparse()。

以事件方式进行处理对于某些操作来说更为自然,如将XML输入转换为另外某种格式。可以使用这个技术将播可列表(来自前面的例子)从XML文件转换为一个CSV文件,以便把它们加载到一个电子表格或数据库应用。

import csv
import sys
from xml.etree.ElementTree import iterparse

writer = csv.writer(sys.stdout,quoting=csv.QUOTE_NONNUMERIC)
group_name = ''

parsing = iterparse('podcasts.opml',events=['start'])

for (event,node) in parsing:
    if node.tag != 'outline':
        # Ignore anything not part of the outline.
        continue
    if not node.attrib.get('xmlUrl'):
        #Remember the current group.
        group_name = node.attrib['text']
    else:
        #Output a podcast entry.
        writer.writerow(
            (group_name,node.attrib['text'],
             node.attrib['xmlUrl'],
             node.attrib.get('htmlUrl',''))
        )

这个转换程序并不需要将整个已解析的输入文件保存在内存中,其在遇到输入中的各个节点时才进行处理,这样做会更为高效。

1.6 创建一个定制树构造器

要处理解析事件,一种可能更高效的方法是将标准的树构造器行为替换为一种定制行为。XMLParser解析器使用一个TreeBuilder处理XML,并调用目标类的方法保存结果。通常输出是由默认TreeBuilder类创建的一个ElementTree实例。可以将TreeBuilder替换为另一个类,使它在实例化Element节点之前接收事件,从而节省这部分开销。 

可以将XML-CSV转换器重新实现为一个树构造器。

import io
import csv
import sys
from xml.etree.ElementTree import XMLParser

class PodcastListToCSV(object):
    def __init__(self,outputFile):
        self.writer = csv.writer(
            outputFile,
            quoting = csv.QUOTE_NONNUMERIC,
        )
        self.group_name = ''

    def start(self,tag,attrib):
        if tag != 'outline':
            # Ignore anything not part of the outline.
            return
        if not attrib.get('xmlUrl'):
            #Remember the current group.
            self.group_name = attrib['text']
        else:
            #Output a pddcast entry.
            self.writer.writerow(
                (self.group_name,
                attrib['text'],
                attrib['xmlUrl'],
                attrib.get('htmlUrl',''))
            )
    def end(self,tag):
        "Ignore closing tags"
    def data(self,data):
        "Ignore data inside nodes"
    def close(self):
        "Nothing special to do here"

target = PodcastListToCSV(sys.stdout)
parser = XMLParser(target=target)
with open('podcasts.opml','rt') as f:
    for line in f:
        parser.feed(line)
parser.close()

PodcastListToCSV实现了TreeBuilder协议。每次遇到一个新的XML标记时,都会调用start()并提供标记名和属性。看到一个结束标记时,会根据这个标记名调用end()。在这二者之间,如果一个节点有内容,则会调用data()(一般认为树构造器会跟踪“当前”节点)。在所有输入都已经被处理时,将调用close()。它会返回一个值,返回给XMLTreeBuilder的用户。

1.7 用元素节点构造文档

除了解析功能,xml.etree.ElementTree还支持由应用中构造的Element对象来创建良构的XML文档。解析文档时使用的Element类还知道如何生成其内容的一个串行化形式,然后可以将这个串行化内容写至一个文件或其他数据流。

有3个辅助函数对于创建Element节点层次结构很有用。Element()创建一个标准节点,SubElement()将一个新节点关联到一个父节点,Comment()创建一个使用XML注释语法串行化数据的节点。

from xml.etree.ElementTree import Element,SubElement,Comment,tostring

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top,'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top,'child_with_tail')
child_with_tail.text = 'This child has text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top,'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print(tostring(top))

这个输出只包含树中的XML节点,而不包含版本和编码的XML声明。

1.8 美观打印XML

ElementTree不会通过格式化tostring()的输出来提高可读性,因为增加额外的空白符会改变文档的内容。为了让输出更易读,后面的例子将使用xml.dom.minidom解析XML,然后使用它的toprettyxml()方法。

from xml.etree import ElementTree
from xml.dom import minidom
from xml.etree.ElementTree import Element,SubElement,Comment,tostring

def prettify(elem):
    """
    Return a pretty-printed XML string for the Element.
    """
    rough_string = ElementTree.tostring(elem,'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

top = Element('top')

comment = Comment('Generated for PyMOTW')
top.append(comment)

child = SubElement(top,'child')
child.text = 'This child contains text.'

child_with_tail = SubElement(top,'child_with_tail')
child_with_tail.text = 'This child has text.'
child_with_tail.tail = 'And "tail" text.'

child_with_entity_ref = SubElement(top,'child_with_entity_ref')
child_with_entity_ref.text = 'This & that'

print(prettify(top))

输出变得更易读。

除了增加用于格式化的额外空白符,xml.dom.minidom美观打印器还会向输出增加一个XML声明。 

posted @ 2020-03-27 11:28  SmallGrayCode  阅读(1539)  评论(0编辑  收藏  举报