您当前位置: 首页 ‣ 深入 Python 3 ‣
难度级别: ♦♦♦♦♢
❝ 在阿里斯塔赫穆斯的执政时期,德拉孔颁布了他的法令。 ❞
— 亚里士多德
本书中几乎所有章节都围绕一段示例代码展开。但 XML 不是关于代码的;它是关于数据的。 XML 的一个常见用途是“联合供稿”,它列出了博客、论坛或其他频繁更新的网站上的最新文章。大多数流行的博客软件可以生成一个供稿,并在发布新文章、讨论主题或博客文章时更新它。您可以通过“订阅”博客的供稿来关注它,并且可以使用专门的“供稿聚合器” (如 Google 阅读器) 来关注多个博客。
那么,这就是我们将在本章中使用的 XML 数据。它是一个供稿——具体来说,是一个 Atom 联合供稿。
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title>
<subtitle>currently between addictions</subtitle>
<id>tag:diveintomark.org,2001-07-29:/</id>
<updated>2009-03-27T21:56:07Z</updated>
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
<link rel='self' type='application/atom+xml' href='http://diveintomark.org/feed/'/>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
<updated>2009-03-27T21:56:07Z</updated>
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/>
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Accessibility is a harsh mistress</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>
<id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
<updated>2009-03-22T01:05:37Z</updated>
<published>2009-03-21T20:09:28Z</published>
<category scheme='http://diveintomark.org' term='accessibility'/>
<summary type='html'>The accessibility orthodoxy does not permit people to
question the value of features that are rarely useful and rarely used.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
</author>
<title>A gentle introduction to video encoding, part 1: container formats</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>
<id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
<updated>2009-01-11T19:39:22Z</updated>
<published>2008-12-18T15:54:22Z</published>
<category scheme='http://diveintomark.org' term='asf'/>
<category scheme='http://diveintomark.org' term='avi'/>
<category scheme='http://diveintomark.org' term='encoding'/>
<category scheme='http://diveintomark.org' term='flv'/>
<category scheme='http://diveintomark.org' term='GIVE'/>
<category scheme='http://diveintomark.org' term='mp4'/>
<category scheme='http://diveintomark.org' term='ogg'/>
<category scheme='http://diveintomark.org' term='video'/>
<summary type='html'>These notes will eventually become part of a
tech talk on video encoding.</summary>
</entry>
</feed>
⁂
如果您已经了解 XML,则可以跳过本节。
XML 是一种描述分层结构化数据的通用方式。一个 XML 文档包含一个或多个元素,这些元素由开始和结束标签分隔。这是一个完整的(尽管很无聊)XML 文档
<foo> ①
</foo> ②
foo
元素的开始标签。foo
元素的匹配结束标签。就像编写、数学或代码中的平衡括号一样,每个开始标签都必须通过相应的结束标签关闭(匹配)。元素可以嵌套到任何深度。元素 bar
在元素 foo
中被称为 foo
的子元素或子级。
<foo>
<bar></bar>
</foo>
每个 XML 文档中的第一个元素称为根元素。一个 XML 文档只能有一个根元素。以下是不是一个 XML 文档,因为它有两个根元素
<foo></foo>
<bar></bar>
元素可以具有属性,它们是名称-值对。属性列在元素的开始标签内,并用空格分隔。属性名称不能在一个元素中重复。属性值必须用引号引起来。您可以使用单引号或双引号。
<foo lang='en'> ①
<bar id='papayawhip' lang="fr"></bar> ②
</foo>
foo
元素有一个名为 lang
的属性。其 lang
属性的值为 en
。bar
元素有两个名为 id
和 lang
的属性。其 lang
属性的值为 fr
。这与 foo
元素没有任何冲突。每个元素都有自己的一组属性。如果一个元素有多个属性,则属性的顺序并不重要。元素的属性形成一组无序的键和值,就像 Python 字典一样。您可以为每个元素定义的属性数量没有限制。
元素可以具有文本内容。
<foo lang='en'>
<bar lang='fr'>PapayaWhip</bar>
</foo>
不包含文本和子元素的元素是空的。
<foo></foo>
有一种编写空元素的简写方式。通过在开始标签中放置一个 /
字符,您可以完全跳过结束标签。上一个示例中的 XML 文档可以改写为
<foo/>
就像 Python 函数可以在不同的模块中声明一样,XML 元素可以在不同的命名空间中声明。命名空间通常看起来像 URL。您使用 xmlns
声明来定义默认命名空间。命名空间声明看起来类似于属性,但它们的用途不同。
<feed xmlns='http://www.w3.org/2005/Atom'> ①
<title>dive into mark</title> ②
</feed>
feed
元素位于 http://www.w3.org/2005/Atom
命名空间中。title
元素也位于 http://www.w3.org/2005/Atom
命名空间中。命名空间声明会影响它所在声明的元素以及所有子元素。您还可以使用 xmlns:prefix
声明来定义命名空间,并将其与前缀相关联。然后,该命名空间中的每个元素都必须用前缀显式声明。
<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'> ①
<atom:title>dive into mark</atom:title> ②
</atom:feed>
feed
元素位于 http://www.w3.org/2005/Atom
命名空间中。title
元素也位于 http://www.w3.org/2005/Atom
命名空间中。就 XML 解析器而言,前两个 XML 文档是相同的。命名空间 + 元素名称 = XML 标识。前缀仅用于引用命名空间,因此实际的前缀名称 (atom:
) 是无关紧要的。命名空间匹配,元素名称匹配,属性(或缺乏属性)匹配,每个元素的文本内容匹配,因此 XML 文档是相同的。
最后,XML 文档可以在根元素之前的首行中包含 字符编码信息。(如果您好奇一个文档如何包含在解析文档之前需要知道的信息,XML 规范的第 F 节详细说明了如何解决这个两难境地。)
<?xml version='1.0' encoding='utf-8'?>
现在,您已经掌握了足够的 XML 知识,可以开始冒险了!
⁂
想想一个网络日志,或者实际上任何具有频繁更新内容的网站,比如 CNN.com。该网站本身有一个标题(“CNN.com”)、一个副标题(“突发新闻、美国、世界、天气、娱乐 & 视频新闻”)、一个最后更新日期(“更新于美国东部时间 2009 年 5 月 16 日下午 12:43”)和一个在不同时间发布的文章列表。每篇文章也都有一个标题、一个首次发布日期(也许还有一个最后更新日期,如果他们发布了更正或修正了错别字),以及一个唯一的 URL。
Atom 联合供稿格式旨在以标准格式捕获所有这些信息。我的网络日志和 CNN.com 在设计、范围和受众方面有很大差异,但它们都具有相同的基本结构。CNN.com 有一个标题;我的博客有一个标题。CNN.com 发布文章;我发布文章。
在顶层是根元素,每个 Atom 供稿都共享:http://www.w3.org/2005/Atom
命名空间中的 feed
元素。
<feed xmlns='http://www.w3.org/2005/Atom' ①
xml:lang='en'> ②
http://www.w3.org/2005/Atom
是 Atom 命名空间。xml:lang
属性,它声明元素及其子元素的语言。在本例中,xml:lang
属性在根元素上声明一次,这意味着整个供稿都是英文的。Atom 供稿包含有关供稿本身的一些信息。这些信息在根级 feed
元素的子元素中声明。
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title> ①
<subtitle>currently between addictions</subtitle> ②
<id>tag:diveintomark.org,2001-07-29:/</id> ③
<updated>2009-03-27T21:56:07Z</updated> ④
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/> ⑤
dive into mark
。currently between addictions
。link
元素没有文本内容,但它有三个属性:rel
、type
和 href
。rel
值告诉您这是一个什么类型的链接;rel='alternate'
表示这是一个指向此供稿的备用表示的链接。type='text/html'
属性表示这是一个指向 HTML 页面的链接。链接目标在 href
属性中给出。现在我们知道这是一个名为“dive into mark” 的网站的供稿,该网站位于 http://diveintomark.org/
,最后更新于 2009 年 3 月 27 日。
☞尽管元素的顺序在某些 XML 文档中可能很重要,但在 Atom 供稿中并不重要。
在供稿级元数据之后是最新文章的列表。文章看起来像这样
<entry>
<author> ①
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title> ②
<link rel='alternate' type='text/html' ③
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id> ④
<updated>2009-03-27T21:56:07Z</updated> ⑤
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/> ⑥
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds ⑦
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry> ⑧
author
元素告诉您谁写了这篇文章:一个名叫马克的家伙,您可以在 http://diveintomark.org/
找到他。(这与供稿元数据中的备用链接相同,但它不必相同。许多网络日志有多个作者,每个作者都有自己的个人网站。)title
元素给出了文章的标题,“Dive into history, 2009 edition”。link
元素给出了这篇文章的 HTML 版本的地址。published
) 和最后修改日期 (updated
)。diveintopython
、docbook
和 html
下。summary
元素简要概述了这篇文章。(还有一个 content
元素,这里没有显示,如果您想在供稿中包含完整的文章文本。)这个 summary
元素具有 Atom 特定的 type='html'
属性,它指定此摘要是 HTML 的片段,而不是纯文本。这一点很重要,因为它包含了 HTML 特定的实体 (—
和 …
),应该将其呈现为 “—” 和 “…” 而不是直接显示。entry
元素的结束标签,表示这篇文章元数据的结束。⁂
Python 可以通过多种方式解析 XML 文档。它有传统的 DOM 和 SAX 解析器,但我将重点介绍一个名为 ElementTree 的不同库。
>>> import xml.etree.ElementTree as etree ① >>> tree = etree.parse('examples/feed.xml') ② >>> root = tree.getroot() ③ >>> root ④ <Element {http://www.w3.org/2005/Atom}feed at cd1eb0>
xml.etree.ElementTree
中。parse()
函数,它可以接收文件名或 类文件对象。此函数会一次解析整个文档。如果内存不足,则可以使用其他方法来增量解析 XML 文档。parse()
函数返回一个表示整个文档的对象。这不是根元素。要获取对根元素的引用,请调用 getroot()
方法。http://www.w3.org/2005/Atom
命名空间中的 feed
元素。此对象的字符串表示形式强化了一个重要观点:XML 元素是其命名空间和标签名称(也称为本地名称)的组合。此文档中的每个元素都位于 Atom 命名空间中,因此根元素表示为 {http://www.w3.org/2005/Atom}feed
。☞ElementTree 将 XML 元素表示为
{namespace}localname
。您将在 ElementTree API 的多个地方看到并使用这种格式。
在 ElementTree API 中,元素的行为类似于列表。列表的项目是元素的子元素。
# continued from the previous example >>> root.tag ① '{http://www.w3.org/2005/Atom}feed' >>> len(root) ② 8 >>> for child in root: ③ ... print(child) ④ ... <Element {http://www.w3.org/2005/Atom}title at e2b5d0> <Element {http://www.w3.org/2005/Atom}subtitle at e2b4e0> <Element {http://www.w3.org/2005/Atom}id at e2b6c0> <Element {http://www.w3.org/2005/Atom}updated at e2b6f0> <Element {http://www.w3.org/2005/Atom}link at e2b4b0> <Element {http://www.w3.org/2005/Atom}entry at e2b720> <Element {http://www.w3.org/2005/Atom}entry at e2b510> <Element {http://www.w3.org/2005/Atom}entry at e2b750>
{http://www.w3.org/2005/Atom}feed
。title
、subtitle
、id
、updated
和 link
) 以及三个 entry
元素。您可能已经猜到了这一点,但我想明确指出:子元素列表仅包含直接子元素。每个 entry
元素都包含它自己的子元素,但这些子元素不包含在列表中。它们将包含在每个 entry
的子元素列表中,但它们不包含在 feed
的子元素列表中。有一些方法可以找到无论嵌套多深都存在的元素;我们将在本章后面介绍两种这样的方法。
XML 不仅仅是元素的集合;每个元素也可以有自己的一组属性。一旦您获得了对特定元素的引用,您就可以轻松地将它的属性作为 Python 字典获取。
# continuing from the previous example >>> root.attrib ① {'{http://www.w3.org/XML/1998/namespace}lang': 'en'} >>> root[4] ② <Element {http://www.w3.org/2005/Atom}link at e181b0> >>> root[4].attrib ③ {'href': 'http://diveintomark.org/', 'type': 'text/html', 'rel': 'alternate'} >>> root[3] ④ <Element {http://www.w3.org/2005/Atom}updated at e2b4e0> >>> root[3].attrib ⑤ {}
attrib
属性是元素属性的字典。这里的原始标记是 <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
。xml:
前缀引用一个内置命名空间,每个 XML 文档都可以使用它,而无需声明它。[4]
在一个以 0 为基准的列表中 — 是 link
元素。link
元素有三个属性:href
、type
和 rel
。[3]
在一个以 0 为基准的列表中 — 是 updated
元素。updated
元素没有属性,所以它的 .attrib
只是一个空字典。⁂
到目前为止,我们已经“从上到下”处理了这个 XML 文档,从根元素开始,获取它的子元素,依此类推,遍历整个文档。但是,许多 XML 的使用需要你找到特定的元素。Etree 也能做到。
>>> import xml.etree.ElementTree as etree >>> tree = etree.parse('examples/feed.xml') >>> root = tree.getroot() >>> root.findall('{http://www.w3.org/2005/Atom}entry') ① [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>] >>> root.tag '{http://www.w3.org/2005/Atom}feed' >>> root.findall('{http://www.w3.org/2005/Atom}feed') ② [] >>> root.findall('{http://www.w3.org/2005/Atom}author') ③ []
findall()
方法找到与特定查询匹配的子元素。(查询格式将在稍后详细说明。)findall()
方法。它在元素的所有子元素中找到所有匹配的元素。但是为什么没有结果呢?虽然可能并不明显,但这个特定的查询只搜索元素的子元素。由于根 feed
元素没有名为 feed
的子元素,所以此查询返回一个空列表。author
元素;实际上,有三个(每个 entry
中一个)。但是这些 author
元素不是根元素的直接子元素;它们是“孙子元素”(字面意思是,子元素的子元素)。如果你想在任何嵌套级别查找 author
元素,你可以做到,但查询格式略有不同。>>> tree.findall('{http://www.w3.org/2005/Atom}entry') ① [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>] >>> tree.findall('{http://www.w3.org/2005/Atom}author') ② []
tree
对象(从 etree.parse()
函数返回)有几个方法,这些方法反映了根元素上的方法。结果与调用 tree.getroot().findall()
方法的结果相同。author
元素。为什么?因为这只是一个 tree.getroot().findall('{http://www.w3.org/2005/Atom}author')
的快捷方式,这意味着“找到所有作为根元素子元素的 author
元素”。author
元素不是根元素的子元素;它们是 entry
元素的子元素。因此查询没有返回任何匹配项。还有一个 find()
方法,它返回第一个匹配的元素。这对于你只期望一个匹配项的情况很有用,或者如果有多个匹配项,你只关心第一个匹配项。
>>> entries = tree.findall('{http://www.w3.org/2005/Atom}entry') ① >>> len(entries) 3 >>> title_element = entries[0].find('{http://www.w3.org/2005/Atom}title') ② >>> title_element.text 'Dive into history, 2009 edition' >>> foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo') ③ >>> foo_element >>> type(foo_element) <class 'NoneType'>
atom:entry
元素。find()
方法接受一个 ElementTree 查询并返回第一个匹配的元素。foo
的元素,因此返回 None
。☞
find()
方法有一个“陷阱”,最终会让你陷入困境。在布尔上下文中,如果 ElementTree 元素对象不包含任何子元素(即len(element)
为 0),则它将被评估为False
。这意味着if element.find('...')
不会测试find()
方法是否找到了匹配的元素;它测试的是该匹配元素是否具有任何子元素!要测试find()
方法是否返回了元素,请使用if element.find('...') is not None
。
确实有一种方法可以搜索后代元素,即子元素、孙子元素以及任何嵌套级别的元素。
>>> all_links = tree.findall('//{http://www.w3.org/2005/Atom}link') ① >>> all_links [<Element {http://www.w3.org/2005/Atom}link at e181b0>, <Element {http://www.w3.org/2005/Atom}link at e2b570>, <Element {http://www.w3.org/2005/Atom}link at e2b480>, <Element {http://www.w3.org/2005/Atom}link at e2b5a0>] >>> all_links[0].attrib ② {'href': 'http://diveintomark.org/', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[1].attrib ③ {'href': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[2].attrib {'href': 'http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress', 'type': 'text/html', 'rel': 'alternate'} >>> all_links[3].attrib {'href': 'http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats', 'type': 'text/html', 'rel': 'alternate'}
//{http://www.w3.org/2005/Atom}link
— 与前面的示例非常相似,除了查询开头有两个斜杠。这两个斜杠意味着“不要只查找直接子元素;我想要所有元素,无论嵌套级别如何”。因此,结果是四个 link
元素的列表,而不仅仅是一个。entry
都有一个 link
子元素,由于查询开头有两个斜杠,因此此查询会找到所有这些元素。总的来说,ElementTree 的 findall()
方法是一个非常强大的功能,但查询语言可能有点令人惊讶。它正式被描述为“对 XPath 表达式的有限支持”。XPath 是一个 W3C 标准,用于查询 XML 文档。ElementTree 的查询语言与 XPath 足够相似,可以进行基本的搜索,但又不完全相同,如果你已经了解 XPath,它可能会让你感到厌烦。现在让我们看一下一个第三方 XML 库,它使用完整的 XPath 支持扩展了 ElementTree API。
⁂
lxml
是一个开源的第三方库,它建立在流行的 libxml2 解析器之上。它提供了一个 100% 兼容的 ElementTree API,然后用完整的 XPath 1.0 支持和一些其他优点扩展它。Windows 上有安装程序可用;Linux 用户应始终尝试使用发行版特定的工具,例如 yum
或 apt-get
,从其存储库安装预编译的二进制文件。否则,你需要手动安装 lxml
。
>>> from lxml import etree ① >>> tree = etree.parse('examples/feed.xml') ② >>> root = tree.getroot() ③ >>> root.findall('{http://www.w3.org/2005/Atom}entry') ④ [<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>, <Element {http://www.w3.org/2005/Atom}entry at e2b510>, <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
lxml
提供与内置 ElementTree 库相同的 API。parse()
函数:与 ElementTree 相同。getroot()
方法:也相同。findall()
方法:完全相同。对于大型 XML 文档,lxml
比内置的 ElementTree 库快得多。如果你只使用 ElementTree API 并想使用最快的可用实现,你可以尝试导入 lxml
并回退到内置的 ElementTree。
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
但 lxml
不仅仅是一个更快的 ElementTree。它的 findall()
方法包含对更复杂表达式的支持。
>>> import lxml.etree ① >>> tree = lxml.etree.parse('examples/feed.xml') >>> tree.findall('//{http://www.w3.org/2005/Atom}*[@href]') ② [<Element {http://www.w3.org/2005/Atom}link at eeb8a0>, <Element {http://www.w3.org/2005/Atom}link at eeb990>, <Element {http://www.w3.org/2005/Atom}link at eeb960>, <Element {http://www.w3.org/2005/Atom}link at eeb9c0>] >>> tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']") ③ [<Element {http://www.w3.org/2005/Atom}link at eeb930>] >>> NS = '{http://www.w3.org/2005/Atom}' >>> tree.findall('//{NS}author[{NS}uri]'.format(NS=NS)) ④ [<Element {http://www.w3.org/2005/Atom}author at eeba80>, <Element {http://www.w3.org/2005/Atom}author at eebba0>]
import lxml.etree
(而不是,例如,from lxml import etree
),以强调这些功能是特定于 lxml
的。href
属性的元素。查询开头的 //
意味着“任何位置的元素(不仅仅是根元素的子元素)”。{http://www.w3.org/2005/Atom}
意味着“仅 Atom 命名空间中的元素”。*
意味着“具有任何本地名称的元素”。而 [@href]
意味着“具有 href
属性”。href
值为 http://diveintomark.org/
的 Atom 元素。uri
元素作为子元素的 Atom author
元素。这只会返回两个 author
元素,即第一个和第二个 entry
中的元素。最后一个 entry
中的 author
仅包含 name
,而不包含 uri
。对你来说还不够吗?lxml
还集成了对任意 XPath 1.0 表达式的支持。我不会深入讨论 XPath 语法;那可以写一整本书!但我会向你展示它如何集成到 lxml
中。
>>> import lxml.etree >>> tree = lxml.etree.parse('examples/feed.xml') >>> NSMAP = {'atom': 'http://www.w3.org/2005/Atom'} ① >>> entries = tree.xpath("//atom:category[@term='accessibility']/..", ② ... namespaces=NSMAP) >>> entries ③ [<Element {http://www.w3.org/2005/Atom}entry at e2b630>] >>> entry = entries[0] >>> entry.xpath('./atom:title/text()', namespaces=NSMAP) ④ ['Accessibility is a harsh mistress']
term
属性(其值为 accessibility
)的 category
元素(在 Atom 命名空间中)。但这实际上不是查询结果。请查看查询字符串的末尾;你注意到 /..
部分了吗?这意味着“然后返回刚找到的 category
元素的父元素”。因此,这个单一的 XPath 查询将找到所有具有 <category term='accessibility'>
子元素的条目。xpath()
函数返回一个 ElementTree 对象列表。在这个文档中,只有一个条目的 category
的 term
是 accessibility
。./
)的子元素 title
元素(atom:title
)的文本内容(text()
)。⁂
Python 对 XML 的支持不仅限于解析现有文档。你还可以从头开始创建 XML 文档。
>>> import xml.etree.ElementTree as etree >>> new_feed = etree.Element('{http://www.w3.org/2005/Atom}feed', ① ... attrib={'{http://www.w3.org/XML/1998/namespace}lang': 'en'}) ② >>> print(etree.tostring(new_feed)) ③ <ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
Element
类。你将元素名称(命名空间 + 本地名称)作为第一个参数传递。此语句在 Atom 命名空间中创建了一个 feed
元素。这将是我们新文档的根元素。{namespace}localname
。tostring()
函数序列化任何元素(及其子元素)。这种序列化是否让你感到惊讶?ElementTree 序列化命名空间 XML 元素的方式在技术上是准确的,但不是最佳的。本章开头处的示例 XML 文档定义了一个默认命名空间(xmlns='http://www.w3.org/2005/Atom'
)。定义默认命名空间对于文档 — 例如 Atom 馈送 — 非常有用,因为这些文档中的每个元素都在同一个命名空间中,因为你只需声明一次命名空间,然后使用其本地名称(<feed>
、<link>
、<entry>
)声明每个元素。除非你想声明来自另一个命名空间的元素,否则无需使用任何前缀。
一个 XML 解析器不会“看到”具有默认命名空间的 XML 文档和具有前缀命名空间的 XML 文档之间的任何区别。此序列化的结果 DOM
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
与此序列化的 DOM 相同
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
唯一的实际区别是,第二种序列化要短几个字符。如果我们将整个示例馈送重新转换为每个开始和结束标签中都有 ns0:
前缀,则将为每个开始标签添加 4 个字符 × 79 个标签 + 4 个字符用于命名空间声明本身,总共 320 个字符。假设使用 UTF-8 编码,则为 320 个额外字节。(压缩后,差异下降到 21 个字节,但仍然,21 个字节是 21 个字节。)也许对你来说这并不重要,但对于像 Atom 馈送这样的东西,它可能在每次更改时下载数千次,每次请求节省几个字节可以很快累积起来。
内置的 ElementTree 库不提供对序列化命名空间元素的这种细粒度控制,但 lxml
可以。
>>> import lxml.etree >>> NSMAP = {None: 'http://www.w3.org/2005/Atom'} ① >>> new_feed = lxml.etree.Element('feed', nsmap=NSMAP) ② >>> print(lxml.etree.tounicode(new_feed)) ③ <feed xmlns='http://www.w3.org/2005/Atom'/> >>> new_feed.set('{http://www.w3.org/XML/1998/namespace}lang', 'en') ④ >>> print(lxml.etree.tounicode(new_feed)) <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
None
作为前缀实际上声明了一个默认命名空间。lxml
特定的 nsmap 参数,lxml
将尊重你定义的命名空间前缀。feed
元素,且没有命名空间前缀。xml:lang
属性了。您可以随时使用set()
方法向任何元素添加属性。它接受两个参数:标准 ElementTree 格式的属性名称,然后是属性值。(此方法不是lxml
特有的。此示例中唯一lxml
特有的部分是用于控制序列化输出中命名空间前缀的nsmap参数。)XML文档是否仅限于每个文档一个元素?当然不是。您也可以轻松地创建子元素。
>>> title = lxml.etree.SubElement(new_feed, 'title', ① ... attrib={'type':'html'}) ② >>> print(lxml.etree.tounicode(new_feed)) ③ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'/></feed> >>> title.text = 'dive into …' ④ >>> print(lxml.etree.tounicode(new_feed)) ⑤ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'>dive into &hellip;</title></feed> >>> print(lxml.etree.tounicode(new_feed, pretty_print=True)) ⑥ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title type='html'>dive into&hellip;</title> </feed>
SubElement
类。唯一必需的参数是父元素(本例中为new_feed)和新元素的名称。由于此子元素将继承其父元素的命名空间映射,因此此处无需重新声明命名空间或前缀。title
元素是在 Atom 命名空间中创建的,它被插入为feed
元素的子元素。由于title
元素没有文本内容,也没有子元素,lxml
将其序列化为一个空元素(使用/>
快捷方式)。.text
属性。title
元素被序列化,并带有其文本内容。任何包含小于号或和号的文本内容都需要在序列化时进行转义。lxml
会自动处理这种转义。lxml
会添加“不重要的空白”,以使输出更易读。☞您可能还想查看 xmlwitch,另一个用于生成XML的第三方库。它广泛使用
with
语句,以使XML生成代码更易读。
⁂
XML规范要求所有符合标准的XML解析器采用“严格的错误处理”。也就是说,一旦检测到XML文档中任何类型的格式错误,它们必须停止并崩溃。格式错误包括不匹配的开始和结束标签、未定义的实体、非法的 Unicode 字符,以及许多其他深奥的规则。这与其他常见格式(如HTML)形成鲜明对比——如果您忘记关闭HTML标签或转义属性值中的和号,您的浏览器不会停止呈现网页。(普遍认为HTML没有定义的错误处理。实际上,HTML错误处理定义得相当完善,但它比“在第一个错误上停止并崩溃”复杂得多。)
有些人(包括我自己)认为,XML发明者强制执行严格的错误处理是一个错误。不要误会我的意思;我当然可以理解简化错误处理规则的吸引力。但在实践中,“格式正确”的概念比它看起来要棘手得多,尤其是对于发布在网上并通过HTTP提供服务的XML文档(如 Atom 订阅源)。尽管XML已经很成熟,并且在 1997 年就将严格的错误处理标准化,但调查结果不断显示,网上有相当一部分 Atom 订阅源存在格式错误。
因此,我从理论和实践上都有理由“不惜一切代价”解析XML文档,也就是说,不要在第一个格式错误上停止并崩溃。如果您也发现自己想这样做,lxml
可以提供帮助。
这是一段损坏的XML文档的片段。我已经突出显示了格式错误。
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into …</title>
...
</feed>
这是一个错误,因为…
实体在XML中没有定义。(它在HTML中定义。)如果您尝试使用默认设置解析此损坏的订阅源,lxml
将在遇到未定义的实体时崩溃。
>>> import lxml.etree >>> tree = lxml.etree.parse('examples/feed-broken.xml') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "lxml.etree.pyx", line 2693, in lxml.etree.parse (src/lxml/lxml.etree.c:52591) File "parser.pxi", line 1478, in lxml.etree._parseDocument (src/lxml/lxml.etree.c:75665) File "parser.pxi", line 1507, in lxml.etree._parseDocumentFromURL (src/lxml/lxml.etree.c:75993) File "parser.pxi", line 1407, in lxml.etree._parseDocFromFile (src/lxml/lxml.etree.c:75002) File "parser.pxi", line 965, in lxml.etree._BaseParser._parseDocFromFile (src/lxml/lxml.etree.c:72023) File "parser.pxi", line 539, in lxml.etree._ParserContext._handleParseResultDoc (src/lxml/lxml.etree.c:67830) File "parser.pxi", line 625, in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:68877) File "parser.pxi", line 565, in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:68125) lxml.etree.XMLSyntaxError: Entity 'hellip' not defined, line 3, column 28
要解析此损坏的XML文档,尽管它存在格式错误,您需要创建一个自定义的XML解析器。
>>> parser = lxml.etree.XMLParser(recover=True) ① >>> tree = lxml.etree.parse('examples/feed-broken.xml', parser) ② >>> parser.error_log ③ examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: Entity 'hellip' not defined >>> tree.findall('{http://www.w3.org/2005/Atom}title') [<Element {http://www.w3.org/2005/Atom}title at ead510>] >>> title = tree.findall('{http://www.w3.org/2005/Atom}title')[0] >>> title.text ④ 'dive into ' >>> print(lxml.etree.tounicode(tree.getroot())) ⑤ <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'> <title>dive into </title> . . [rest of serialization snipped for brevity] .
lxml.etree.XMLParser
类。它可以接受许多不同的命名参数。我们在这里感兴趣的是recover参数。当设置为True
时,XML解析器将尽力从格式错误中“恢复”。parse()
函数。请注意,lxml
不会抛出关于未定义的…
实体的异常。…
实体,解析器只是默默地将其删除了。title
元素的文本内容变为'dive into '
。…
实体并没有被移动,而是被删除了。需要重申的是,使用“恢复”XML解析器无法保证互操作性。另一个解析器可能会认为它识别出了HTML中的…
实体,并将其替换为&hellip;
。这是“更好”的吗?也许。它是“更正确”的吗?不,它们同样不正确。正确的行为(根据XML规范)是停止并崩溃。如果您决定不这样做,那么您将独自承担后果。
⁂
lxml
lxml
解析XML和HTMLlxml
进行 XPath 和XSLT© 2001–11 Mark Pilgrim