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

作者:@小灰灰
本文为作者原创,转载请注明出处:https://www.cnblogs.com/liuhui0308/p/12570390.html


阅读目录(Content)

  • 1. xml.etree.ElementTree XML操纵API
    • 1.1 解析XML文档
    • 1.2 遍历解析树
    • 1.3 查找文档中的节点
    • 1.4 解析节点属性
    • 1.5 解析时监视事件
    • 1.6 创建一个定制树构造器
    • 1.7 用元素节点构造文档
    • 1.8 美观打印XML
回到顶部(go to top)

1. xml.etree.ElementTree XML操纵API

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

1.1 解析XML文档

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

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

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

podcasts.opml:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <opml version="1.0">
  3. <head>
  4. <title>My Podcasts</title>
  5. <dateCreated>Sat, 06 Aug 2016 15:53:26 GMT</dateCreated>
  6. <dateModified>Sat, 06 Aug 2016 15:53:26 GMT</dateModified>
  7. </head>
  8. <body>
  9. <outline text="Non-tech">
  10. <outline
  11. text="99% Invisible" type="rss"
  12. xmlUrl="http://feeds.99percentinvisible.org/99percentinvisible"
  13. htmlUrl="http://99percentinvisible.org" />
  14. </outline>
  15. <outline text="Python">
  16. <outline
  17. text="Talk Python to Me" type="rss"
  18. xmlUrl="https://talkpython.fm/episodes/rss"
  19. htmlUrl="https://talkpython.fm" />
  20. <outline
  21. text="Podcast.__init__" type="rss"
  22. xmlUrl="http://podcastinit.podbean.com/feed/"
  23. htmlUrl="http://podcastinit.com" />
  24. </outline>
  25. </body>
  26. </opml>

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

  1. from xml.etree import ElementTree
  2. with open('podcasts.opml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. print(tree)

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

1.2 遍历解析树

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

  1. from xml.etree import ElementTree
  2. with open('podcasts.opml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. for node in tree.iter():
  5. print(node.tag)

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

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

  1. from xml.etree import ElementTree
  2. with open('podcasts.opml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. for node in tree.iter('outline'):
  5. name = node.attrib.get('text')
  6. url = node.attrib.get('xmlUrl')
  7. if name and url:
  8. print(' %s' % name)
  9. print(' %s' % url)
  10. else:
  11. print(name)

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

1.3 查找文档中的节点

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

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

  1. from xml.etree import ElementTree
  2. with open('podcasts.opml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. for node in tree.findall('.//outline'):
  5. url = node.attrib.get('xmlUrl')
  6. if url:
  7. print(url)

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

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

  1. from xml.etree import ElementTree
  2. with open('podcasts.opml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. for node in tree.findall('.//outline/outline'):
  5. url = node.attrib.get('xmlUrl')
  6. print(url)

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

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

1.4 解析节点属性

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

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

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

  1. from xml.etree import ElementTree
  2. with open('data.xml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. node = tree.find('./with_attributes')
  5. print(node.tag)
  6. for name,value in sorted(node.attrib.items()):
  7. print(name,value)

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

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

  1. from xml.etree import ElementTree
  2. with open('data.xml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. for path in ['./child','./child_with_tail']:
  5. node = tree.find(path)
  6. print(node.tag)
  7. print('child node text:',node.text)
  8. print('and tail text:',node.tail)

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

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

  1. from xml.etree import ElementTree
  2. with open('data.xml', 'rt') as f:
  3. tree = ElementTree.parse(f)
  4. node = tree.find('entity_expansion')
  5. print(node.tag)
  6. print('in attribute:',node.attrib['attribute'])
  7. print('in text:',node.text.strip())

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

1.5 解析时监视事件

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

有以下事件类型:

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

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

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

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

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

  1. from xml.etree.ElementTree import iterparse
  2. depth = 0
  3. prefix_width = 8
  4. prefix_dots = '.' * prefix_width
  5. line_template = '.'.join([
  6. '{prefix:<0.{prefix_len}}',
  7. '{event:<8}',
  8. '{suffix:<{suffix_len}}',
  9. '{node.tag:<12}',
  10. '{node_id}',
  11. ])
  12. EVENT_NAMES = ['start','end','start-ns','end-ns']
  13. for (event,node) in iterparse('podcasts.opml',EVENT_NAMES):
  14. if event == 'end':
  15. depth -= 1
  16. prefix_len = depth * 2
  17. print(line_template.format(
  18. prefix = prefix_dots,
  19. prefix_len = prefix_len,
  20. suffix = '',
  21. suffix_len = (prefix_width - prefix_len),
  22. node = node,
  23. node_id = id(node),
  24. event = event,
  25. ))
  26. if event == 'start':
  27. depth += 1

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

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

  1. import csv
  2. import sys
  3. from xml.etree.ElementTree import iterparse
  4. writer = csv.writer(sys.stdout,quoting=csv.QUOTE_NONNUMERIC)
  5. group_name = ''
  6. parsing = iterparse('podcasts.opml',events=['start'])
  7. for (event,node) in parsing:
  8. if node.tag != 'outline':
  9. # Ignore anything not part of the outline.
  10. continue
  11. if not node.attrib.get('xmlUrl'):
  12. #Remember the current group.
  13. group_name = node.attrib['text']
  14. else:
  15. #Output a podcast entry.
  16. writer.writerow(
  17. (group_name,node.attrib['text'],
  18. node.attrib['xmlUrl'],
  19. node.attrib.get('htmlUrl',''))
  20. )

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

1.6 创建一个定制树构造器

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

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

  1. import io
  2. import csv
  3. import sys
  4. from xml.etree.ElementTree import XMLParser
  5. class PodcastListToCSV(object):
  6. def __init__(self,outputFile):
  7. self.writer = csv.writer(
  8. outputFile,
  9. quoting = csv.QUOTE_NONNUMERIC,
  10. )
  11. self.group_name = ''
  12. def start(self,tag,attrib):
  13. if tag != 'outline':
  14. # Ignore anything not part of the outline.
  15. return
  16. if not attrib.get('xmlUrl'):
  17. #Remember the current group.
  18. self.group_name = attrib['text']
  19. else:
  20. #Output a pddcast entry.
  21. self.writer.writerow(
  22. (self.group_name,
  23. attrib['text'],
  24. attrib['xmlUrl'],
  25. attrib.get('htmlUrl',''))
  26. )
  27. def end(self,tag):
  28. "Ignore closing tags"
  29. def data(self,data):
  30. "Ignore data inside nodes"
  31. def close(self):
  32. "Nothing special to do here"
  33. target = PodcastListToCSV(sys.stdout)
  34. parser = XMLParser(target=target)
  35. with open('podcasts.opml','rt') as f:
  36. for line in f:
  37. parser.feed(line)
  38. parser.close()

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

1.7 用元素节点构造文档

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

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

  1. from xml.etree.ElementTree import Element,SubElement,Comment,tostring
  2. top = Element('top')
  3. comment = Comment('Generated for PyMOTW')
  4. top.append(comment)
  5. child = SubElement(top,'child')
  6. child.text = 'This child contains text.'
  7. child_with_tail = SubElement(top,'child_with_tail')
  8. child_with_tail.text = 'This child has text.'
  9. child_with_tail.tail = 'And "tail" text.'
  10. child_with_entity_ref = SubElement(top,'child_with_entity_ref')
  11. child_with_entity_ref.text = 'This & that'
  12. print(tostring(top))

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

1.8 美观打印XML

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

  1. from xml.etree import ElementTree
  2. from xml.dom import minidom
  3. from xml.etree.ElementTree import Element,SubElement,Comment,tostring
  4. def prettify(elem):
  5. """
  6. Return a pretty-printed XML string for the Element.
  7. """
  8. rough_string = ElementTree.tostring(elem,'utf-8')
  9. reparsed = minidom.parseString(rough_string)
  10. return reparsed.toprettyxml(indent=" ")
  11. top = Element('top')
  12. comment = Comment('Generated for PyMOTW')
  13. top.append(comment)
  14. child = SubElement(top,'child')
  15. child.text = 'This child contains text.'
  16. child_with_tail = SubElement(top,'child_with_tail')
  17. child_with_tail.text = 'This child has text.'
  18. child_with_tail.tail = 'And "tail" text.'
  19. child_with_entity_ref = SubElement(top,'child_with_entity_ref')
  20. child_with_entity_ref.text = 'This & that'
  21. print(prettify(top))

输出变得更易读。

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

(0)

相关推荐

  • xpath语法简介

    有多种方式可以从网页中提取我们需要的信息,既可以通过正则表达式,也可以使用BeautifulSoup模块.除此之外,xpath表达式也是一种常见用法. xpath称之为xml路径语言,是一种基于xml ...

  • Python 标准库之 xml.etree.ElementTree

    一.导入ET 在Python标准库中,ElementTree有两种实现方式:一种是纯Python的实现xml.etree.ElementTree,另一种是速度更快一点的xml.etree.cEleme ...

  • python3常用标准库

    python3常用标准库 趁着有时间,把一些我用过的常用标准库进行整理和复习. time 用法 说明 time.time() 返回时间戳(从1970年1月1日00:00:00开始计算) time.lo ...

  • 用Python标准库turtle画一头金牛,祝您新年牛气冲天!(附源码)

    今年是牛年,祝大家新年牛气冲天!嗨皮牛Year! 前几天在百度图片里下载了一张金牛的图片,就是封面的这张.想着用Python标准库turtle肯定可以画出这张图,所以说干就干,花两天时间实现了. 画图 ...

  • Python标准库模块之heapq

    该模块提供了堆排序算法的实现.堆是二叉树,最大堆中父节点大于或等于两个子节点,最小堆父节点小于或等于两个子节点. 创建堆 heapq有两种方式创建堆, 一种是使用一个空列表,然后使用heapq.hea ...

  • C/C++标准库之转换UTC时间到local本地时间详解

    前言 UTC 时间DateTime.UtcNow 和 系统本地时间 DateTime.Now 相差8个时区 ,美国本地时间和北京时间相差15个时区: 美国,而一般使用UTC时间方便统一各地区时间差异. ...

  • 如何避免C标准库缓冲区溢出

    C中大多数缓冲区溢出问题可以直接追溯到标准 C 库.最有害的罪魁祸首是不进行自变量检查的.有问题的字符串操作strcpy.strcat.sprintf 和 gets. 大部分程序员仍然会使用这些函数, ...

  • C语言这些常用的标准库(头文件),你不得不知道...

    有很多工程师喜欢自己封装一些标准库已有的函数,其实自己封装的函数,并不一定比标准库好,有时候反而代码更冗余,且有bug. 下面小编就来分享一下C语言常见的一些标准库. 标准头文件包括: <ass ...

  • Go标准库之html/template

    html/template包实现了数据驱动的模板,用于生成可防止代码注入的安全的HTML内容.它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用html/templ ...

  • C++标准库详解

    逆风飞扬2011-08-25 09:24:03 C++标准库的所有头文件都没有扩展名.C++标准库的内容总共在50个标准头文件中定义,其中18个提供了C库的功能. <cname>形式的标准 ...