lxml.etree Tutorial 官方教学文档全文翻译

lxml.etree是Python中处理XML的主要工具,本文档翻译了其官方教学内容,涵盖了Element类、属性、文本处理、XPath、命名空间、E-工厂等关键知识点。Element几乎模仿了列表行为,支持以字典方式处理属性,可使用XPath查找文本。lxml还提供了递增解析和事件驱动解析,以及命名空间和ElementPath等高级功能。
摘要由CSDN通过智能技术生成

lxml.etree 官方教学文档翻译


文档地址:
lxml.etree Tutorial

教学文档

>>> from lxml import etree
>>> etree.LXML_VERSION
(4, 5, 1, 0)

Element

ElementElementTree API 的主要容器对象,大部分有关 XML 树的功能可通过该类访问。

Element可以通过工厂方法创建。

>>> root = etree.Element("root")

调用时须指定_tag参数,作为实例的tag属性,该属性用作 XML 节点开始与结束标签的文本内容。

>>> type(root)
<class 'lxml.etree._Element'>
>>> print(root.tag)
root
>>> print(etree.tostring(root, pretty_print=True))
<root>
</root>

该类以 XML 树的形式组织,可以通过append()方法添加新的实例作为子节点。

>>> root.append( etree.Element("child1") )

通过SubElement()工厂方法能更简单地添加子节点,不必额外创建,其参数包括父节点作为首参以及工厂方法Element()的全部参数。

>>> child2 = etree.SubElement(root, "child2")
>>> child3 = etree.SubElement(root, "child3")

通过打印 root 实例可以印证 XML 树的创建。

>>> print(etree.tostring(root, pretty_print=True))
<root>
  <child1/>
  <child2/>
  <child3/>
</root>
Element几乎是列表

为了可以简洁直接地访问子节点,Element尽可能地模仿 Python List 的行为。

包括索引访问、接口函数(len(), index(), list(), insert() )、for loop 遍历、索引切片等。

>>> child = root[0]
>>> print(child.tag)
child1

>>> print(len(root))
3

>>> root.index(root[1]) # lxml.etree only!
1

>>> children = list(root)

>>> for child in root: # root is iterable!
...     print(child.tag)
child1
child2
child3

>>> root.insert(0, etree.Element("child0"))
>>> start = root[:1]
>>> end   = root[-1:]

>>> print(start[0].tag)
child0
>>> print(end[0].tag)
child3

虽然尽力模仿,但Element毕竟仍不是列表,一些在早期版本中实现的仿列表特性现已不再支持。

ElementTree 1.3lxml 2.0 版本中,能通过检查Element的真值判断是否含有子节点,即子节点列表是否为空:

if root:   # this no longer works!
    print("The root element has children")

这种方法存在缺陷:人们通常希望求值对象实例得到 True ,无论其是否含有子节点,因而会对上述if语句中判断为 False 的结果感到意外。而使用len(Element)能显式地实现同样的效果,报错也更少。

>>> print(etree.iselement(root))  # test if it's some kind of Element
True
>>> if len(root):                 # test if it has children
...     print("The root element has children")
The root element has children

lxml 2.0 中也修改了Element的行为,抛弃了原始 ElementTree 1.3Python 2.7 中的某些仿列表特性:

>>> for child in root:
...     print(child.tag)
child0
child1
child2
child3
>>> root[0] = root[-1]  # this moves the element in lxml.etree!
>>> for child in root:
...     print(child.tag)
child3
child1
child2

此例中,最后的子节点被移动而非拷贝至不同的位置,就是说,该子节点被放入其他位置时从原本的位置自动移除了。列表中的对象可以同时出现在多个位置,下面代码中的赋值操作也只会将列表中最后元素的引用拷贝至第一个位置,因而二者能包含相同的元素:

>>> l = [0, 1, 2, 3]
>>> l[0] = l[-1]
>>> l
[3, 1, 2, 3]

应当指出,原始的 ElementTree 中单一的Element对象可以放置在任意多的树的任意多的位置,意味着Element允许与列表相同的拷贝操作。但是明显的缺点在于无论是有意还是无意,对原对象的修改会同步到所有位置。

更高层的差异在于 lxml.etreeElement最多只能有一个父节点,该父节点能被getparent()方法查询,而这种特性在原始 ElementTree 中不受支持。

>>> root is root[0].getparent()  # lxml.etree only!
True

如果你希望拷贝某实例至 lxml.etree 的不同位置,考虑使用 Python 标准库 copy 模块创建独立的深度拷贝 (deep copy):

>>> from copy import deepcopy

>>> element = etree.Element("neu")
>>> element.append( deepcopy(root[1]) )

>>> print(element[0].tag)
child1
>>> print([ c.tag for c in root ])
['child3', 'child1', 'child2']  # child1 not removed!

Element的兄弟节点可以作为nextprevious节点被访问:

>>> root[0] is root[1].getprevious() # lxml.etree only!
True
>>> root[1] is root[0].getnext() # lxml.etree only!
True
Element以字典形式携带属性

XML 节点支持属性,你可以直接使用Element()工厂方法生成带属性的Element

>>> root = etree.Element("root", interesting="totally")
>>> etree.tostring(root)
b'<root interesting="totally"/>'

属性不过是无序键值对,因而很方便实现处理节点属性的仿字典接口(get(), set(), keys(), items())。

>>> print(root.get("interesting"))
totally

>>> print(root.get("hello"))
None
>>> root.set("hello", "Huhu")
>>> print(root.get("hello"))
Huhu

>>> etree.tostring(root)
b'<root interesting="totally" hello="Huhu"/>'

>>> sorted(root.keys())
['hello', 'interesting']

>>> for name, value in sorted(root.items()):
...     print('%s = %r' % (name, value))
hello = 'Huhu'
interesting = 'totally'

如果你希望查阅键值对或想对属性执行一些真正的字典操作,你可以直接访问attrib属性。

>>> attributes = root.attrib

>>> print(attributes["interesting"])
totally
>>> print(attributes.get("no-such-attribute"))
None

>>> attributes["hello"] = "Guten Tag"
>>> print(attributes["hello"])
Guten Tag
>>> print(root.get("hello"))
Guten Tag

注意到对 attrib 属性的访问是对键值对引用的访问,而非拥有独立的拷贝。因此,任何对 Element 的操作都可能影响该属性,反之,对属性的操作也会同样改变原实例。此外,如果 XML 树任一节点的 attrib 属性仍被访问,那么该树就会持续存留在内存中。为避免副作用,建议拿到一份独立于原属性的字典副本。

>>> d = dict(root.attrib)
>>> sorted(d.items())
[('hello', 'Guten Tag'), ('interesting', 'totally')]
Element包含文本

Element可包含文本:

>>> root = etree.Element("root")
>>> root.text = "TEXT"

>>> print(root.text)
TEXT

>>> etree.tostring(root)
b'<root>TEXT</root>'

大部分 XML 文档(数据型文档),文本往往被封装于树层次结构最底层的叶节点。

而在文本标签化的文档(如 HTML)中,文本也可能出现在树的中间层级:

<html><body>Hello<br/>World</body></html>

<br/>节点被文本环绕,这常被称为文档样式型混合内容 XMLElement类通过tail属性支持该类型,其包含了树中该节点之后直至下一节点的全部文本。

>>> html = etree.Element("html")
>>> body = etree.SubElement(html, "body")
>>> body.text = "TEXT"

>>> etree.tostring(html)
b'<html><body>TEXT</body></html>'

>>> br = etree.SubElement(body, "br")
>>> etree.tostring(html)
b'<html><body>TEXT<br/></body></html>'

>>> br.tail = "TAIL"
>>> etree.tostring(html)
b'<html><body>TEXT<br/>TAIL</body></html>'

texttail属性足以表示 XML 树文档中任何文本。基于此,ElementTree API 不需要使用(你在经典的 DOM APIs 所见的那般)经常碍事的额外文本节点

不过,tail属性也有麻烦的时候。当你希望序列化树中的某一节点,并不希望在结果中见到tail文本(但可能想要其子节点的tail文本)。为实现这一目的,可使用接收with_tail参数的tostring()函数:

>>> etree.tostring(br)
b'<br/>TAIL'
>>> etree.tostring(br, with_tail=False) # lxml.etree only!
b'<br/>'

若你还希望仅读取文本而忽略标签,以正确的顺序递归地串联texttail文本,tostring()函数也能接收method关键字满足你的要求:

>>> etree.tostring(html, method="text")
b'TEXTTAIL'
使用 XPath 寻找文本

XPath 用于提取树中的文本内容,允许将独立文本块提取至列表。

>>> print(html.xpath("string()")) # lxml.etree only!
TEXTTAIL
>>> print(html.xpath("//text()")) # lxml.etree only!
['TEXT', 'TAIL']

还可封装为函数:

>>> build_text_list = etree.XPath("//text()") # lxml.etree only!
>>> print(build_text_list(html))
['TEXT', 'TAIL']

注意 XPath 返回的字符串结果是知晓其来源的“智能”对象,你能通过其getparent()方法得知来源:

>>> texts = build_text_list(html)
>>> print(texts[0])
TEXT
>>> parent = texts[0].getparent()
>>> print(parent.tag)
body

>>> print(texts[1])
TAIL
>>> print(texts[1].getparent().tag)
br

你还可以判断其是text还是tail

>>> print(texts[0].is_text)
True
>>> print(texts[1].is_text)
False
>>> print(texts[1].is_tail)
True

事实上,这一原则仅适用于 XPathtext()函数,而对 XPath 函数string()concat()构建的字符串无效:

>>> stringify = etree.XPath("string()")
>>> print(stringify(html))
TEXTTAIL
>>> print(stringify(html).getparent())
None
树的迭代

书接上文,若你希望递归遍历树并操作节点,对树进行迭代是方便的选择。Element提供一个树迭代器tree iterator。该迭代器以文档序(即序列化后节点的标签在树中出现的顺序)对每一节点执行yield操作:

>>> root = etree.Element("root")
>>> etree.SubElement(root, "child").text = "Child 1"
>>> etree.SubElement(root, "child").text = "Child 2"
>>> etree.SubElement(root, "another").text = "Child 3"

>>> print(etree.tostring(root, pretty_print=True))
<root>
  <child>Child 1</child>
  <child>Child 2</child>
  <another>Child 3</another>
</root>

>>> for element in root.iter():
...     print("%s - %s" % (element.tag, element.text))
root - None
child - Child 1
child - Child 2
another - Child 3

若你只对单一标签感兴趣,可将其名称传递至iter()以迭代。lxml 3.0 开始,你在迭代过程中可以传递多个标签,从而实现对多标签的截取。

>>> for element in root.iter("child"):
...     print("%s - %s" % (element.tag, element.text))
child - Child 1
child - Child 2

>>> for element in root.iter("another", "child"):
...     print("%s - %s" % (element.tag, element.text))
child - Child 1
child - Child 2
another - Child 3

迭代过程默认yield树中的全部节点,包括指令ProcessingInstruction、注释CommentEntity实例。

如果你希望只返回Element对象,你可以将Element工厂作为标签参数传递:

>>> root.append(etree.Entity("#234"))
>>> root.append(etree.Comment("some comment"))

>>> for element in root.iter():
...     if isinstance(element.tag, basestring):  # or 'str' in Python 3
...         print("%s - %s" % (element.tag, element.text))
...     else:
...         print("SPECIAL: %s - %s" % (element, element.text))
root - None
child - Child 1
child - Child 2
another - Child 3
SPECIAL: &#234; - &#234;
SPECIAL: <!--some comment--> - some comment

>>> for element in root.iter(tag=etree.Element):
...     print("%s - %s" % (element.tag, element.text))
root - None
child - Child 1
child - Child 2
another - Child 3

>>> for element in root.iter(tag=etree.Entity):
...     print(element.text)
&#234;

记住传递通配符“ * ”标签名同样会yield所有Element节点(仅Element节点)。

lxml.etree 中,在各个方向(如子节点、父节点、兄弟节点)提供了更深入的迭代器

序列化 Serialisation

序列化常使用tostring()函数返回字符串,或使用ElementTree.write()方法写入文件、文件样对象、甚至 URL(通过 FTP PUTHTTP POST)。两种调用都接收相同的参数如针对格式化输出的pretty_print及选择 plain ASCII 之外其他输出编码的encoding

>>> root = etree.XML('<root><a><b/></a></root>')

>>> etree.tostring(root)
b'<root><a><b/></a></root>'

>>> print(etree.tostring(root, xml_declaration=True))
<?xml version='1.0' encoding='ASCII'?>
<root><a><b/></a></root>

>>> print(etree.tostring(root, encoding='iso-8859-1'))
<?xml version='1.0' encoding='iso-8859-1'?>
<root><a><b/></a></root>

>>> print(etree.tostring(root, pretty_print=True))
<root>
  <a>
    <b/>
  </a>
</root>

注意pretty_print在末尾添加新行。

关于比pretty_print更细粒度的控制,可参考 lxml 4.5 添加的indent()函数在序列化之前向树添加空格缩进:

>>> root = etree.XML('<root><a><b/>\n</a></root>')
>>> print(etree.tostring(root))
<root><a><b/>
</a></root>

>>> etree.indent(root)
>>> print(etree.tostring(root))
<root>
  <a>
    <b/>
  </a>
</root>

>>> root.text
'\n  '
>>> root[0].text
'\n    '

>>> etree.indent(root, space="    ")
>>> print(etree.tostring(root))
<root>
    <a>
        <b/>
    </a>
</root>

>>> etree.indent(root, space="\t")
>>> etree.tostring(root)
'<root>\n\t<a>\n\t\t<b/>\n\t</a>\n</root>'

lxml 2.0ElementTree 1.3 及之后的版本中,序列化函数可以处理的不止 XML。你能传递 method 关键字将对象序列化为 HTML 或提取文本内容:

>>> root = etree.XML(
...    '<html><head/><body><p>Hello<br/>World</p></body></html>')

>>> etree.tostring(root) # default: method = 'xml'
b'<html><head/><body><p>Hello<br/>World</p></body></html>'

>>> etree.tostring(root, method='xml') # same as above
b'<html><head/><body><p>Hello<br/>World</p></body></html>'

>>> etree.tostring(root, method='html')
b'<html><head></head><body><p>Hello<br>World</p></body></html>'

>>> print(etree.tostring(root, method='html', pretty_print=True))
<html>
<head></head>
<body><p>Hello<br>World</p></body>
</html>

>>> etree.tostring(root, method='text')
b'HelloWorld'

XML 序列化中普通文本的默认编码是 ASCII

>>> br = next(root.iter('br'))  # get first result of iteration
>>> br.tail = u'W\xf6rld'

>>> etree.tostring(root, method='text')  # doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' ...

>>> etree.tostring(root, method='text', encoding="UTF-8")
b'HelloW\xc3\xb6rld'

序列化为 Pythonunicode 字符串而非 byte 字符串非常轻松,只需传递unicode作为encoding参数:

>>> etree.tostring(root, encoding='unicode', method='text')
u'HelloW\xf6rld'

W3C 有篇关于 Unicode 字符集和字符串编码的优秀文章

ElementTree

ElementTree主要是对含根节点的树的文档包装器,提供了数种序列化和一般性文档处理的方法。

>>> root = etree.XML('''\
... <?xml version="1.0"?>
... <!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]>
... <root>
...   <a>&tasty;</a>
... </root>
... ''')

>>> tree = etree.ElementTree(root)
>>> print(tree.docinfo.xml_version)
1.0
>>> print(tree.docinfo.doctype)
<!DOCTYPE root SYSTEM "test">

>>> tree.docinfo.public_id = '-//W3C//DTD XHTML 1.0 Transitional//EN'
>>> tree.docinfo.system_url = 'file://local.dtd'
>>> print(tree.docinfo.doctype)
<!DOCTYPE root PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "file://local.dtd">

ElementTree也是调用parse()函数以解析文件或文件样对象时返回的对象。(详情见后文 parsing 部分)

ElementTree与单纯的Element的重要不同之一在于前者视对象为完整的文档进行序列化,包括顶层的程序指令和注释,以及 DOCTYPE 和文档中其他 DTD 内容:

>>> print(etree.tostring(tree))  # lxml 1.3.4 and later
<!DOCTYPE root PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "file://local.dtd" [
<!ENTITY tasty "parsnips">
]>
<root>
  <a>parsnips</a>
</root>

原始的 xml.etree.ElementTree 实现及 lxml 1.3.3 中,输出和序列化根节点是相同的:

>>> print(etree.tostring(tree.getroot()))
<root>
  <a>parsnips</a>
</root>

这种序列化行为在 lxml 1.3.4 中得到改变。在此之前,树的序列化不包含 DTD 内容,lxml 会在输入输出周期中丢失 DTD 信息。

解析字符串与文件

lxml.etree 支持对来自多种关键源(字符串、文件、HTTP/FTP URL和文件样对象)的 XML 进行多种方式的解析。主要的解析函数是fromstring()parse(),二者都以源作为首个参数被调用。函数默认使用标准解析器,但你总能将其他解析器通过第二个参数传递。

fromstring()函数

解析字符串最简单的方式:

>>> some_xml_data = "<root>data</root>"

>>> root = etree.fromstring(some_xml_data)
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'
XML()函数

XML()函数的行为类似于fromstring()函数,但常用于向源中写入 XML 文本:

>>> root = etree.XML("<root>data</root>")
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'

也有为 HTML 文本对应的函数HTML()

>>> root = etree.HTML("<p>data</p>")
>>> etree.tostring(root)
b'<html><body><p>data</p></body></html>'
Parse()函数

Parse()函数用于解析文件和文件样对象。

作为文件样对象的例子,如下代码使用BytesIO类读取字符串而非外部文件。该类来自 Python 2.6 版本之后的io模块。在更早的版本中,你必须使用StringIO模块的StringIO类。然而实践中,你会避免使用以上这些而选择前文的字符串解析函数。

>>> from io import BytesIO
>>> some_file_or_file_like_object = BytesIO(b"<root>data</root>")

>>> tree = etree.parse(some_file_or_file_like_object)

>>> etree.tostring(tree)
b'<root>data</root>'

请注意Parse()函数毕竟是字符串函数,返回ElementTree对象而非Element对象:

>>> root = tree.getroot()
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'

这种差异背后的原因在于parse()从文件中返回完整的文档,而字符串解析函数则常用于解析 XML 片段。

Parse()函数支持以下任一源:

  • 打开的文件对象
  • 拥有.read(byte_count)方法的文件样对象,每次调用返回字符串字节
  • 文件名字符串
  • HTTPFTP URL 字符串

注意到传递文件名或 URL 通常比传递打开的文件或文件样对象更快。然而,libxml2HTTP/FTP 客户端相当简单,因此 HTTP 认证之类的事务需要专门的 URL 请求库,如urllib2requests。这些库通常提供文件样对象作为结果,你能在响应流进入的同时进行解析。

Parse对象

lxml.etree 默认使用附带安装的标准解析器。若你希望配置解析器,你可以创建新的实例:

>>> parser = etree.XMLParser(remove_blank_text=True) # lxml.etree only!

该语句能创建解析同时去除标签间空文本的解析器,从而减小树的尺寸,若你确认只有空格的文本没有意义,这条语句还能避免危险的tail文本。例:

>>> root = etree.XML("<root>  <a/>   <b>  </b>     </root>", parser)

>>> etree.tostring(root)
b'<root><a/><b>  </b></root>'

注意<b>标签间空格内容并未被移除,因叶节点的文本内容可能是数据(即使为空)。你可以额外遍历树从而轻松地去除:

>>> for element in root.iter("*"):
...     if element.text is not None and not element.text.strip():
...         element.text = None

>>> etree.tostring(root)
b'<root><a/><b/></root>'

help(etree.XMLParser)以获得可用的解析器选项。

递增解析

lxml.etree 提供两种逐步递增的解析方式。第一种是使文件样对象反复调用read()方法。当数据源是urllib或其他任何能响应请求提供数据的源时最推荐使用。注意这种情景下数据不可用时解析器会自动阻塞和等待:

>>> class DataSource:
...     data = [ b"<roo", b"t><", b"a/", b"><", b"/root>" ]
...     def read(self, requested_size):
...         try:
...             return self.data.pop(0)
...         except IndexError:
...             return b''

>>> tree = etree.parse(DataSource())

>>> etree.tostring(tree)
b'<root><a/></root>'

第二种方式依赖于“投喂”解析器接口,由feed(data)close()方法实现:

>>> parser = etree.XMLParser()

>>> parser.feed("<roo")
>>> parser.feed("t><")
>>> parser.feed("a/")
>>> parser.feed("><")
>>> parser.feed("/root>")

>>> root = parser.close()

>>> etree.tostring(root)
b'<root><a/></root>'

这里,你任何时刻都能中断解析过程,而后再次调用feed()方法继续解析过程。如果你想避免如 Twisted 框架中的阻塞调用,或是数据流缓慢、或成块到达而你希望在等待下一块的间隙做其他工作时,feed()尤其方便。

调用close()方法后(或解析器抛出异常时),你也可以调用feed()方法复用解析器:

>>> parser.feed("<root/>")
>>> root = parser.close()
>>> etree.tostring(root)
b'<root/>'
事件驱动解析

有时,你只想从深深的文档树中获得一小片段,所以将整个树载入内存、遍历后销毁的解析过程未免开销过大。lxml.etree 以两处事件驱动的解析器接口处理这种应用场景:一种建立树iterparse的同时生成解析器事件,另一种根本不建立树而对 SAX (Simple API for XML) 样式的目标对象调用反馈方法。

以下是iterparse()函数的简单例子:

>>> some_file_like = BytesIO(b"<root><a>data</a></root>")

>>> for event, element in etree.iterparse(some_file_like):
...     print("%s, %4s, %s" % (event, element.tag, element.text))
end,    a, data
end, root, None

iterparse()默认生成解析节点结束的事件,但你能通过events参数加以控制:

>>> some_file_like = BytesIO(b"<root><a>data</a></root>")

>>> for event, element in etree.iterparse(some_file_like,
...                                       events=("start", "end")):
...     print("%5s, %4s, %s" % (event, element.tag, element.text))
start, root, None
start,    a, data
  end,    a, data
  end, root, None

当接收到start事件时text/tail/Element子节点并未保证出现,只有end事件确保节点已被彻底解析。

也允许.clear()或修改Element的内容以节省内存。因而如果你解析一颗大型树并且你希望内存占用小,就应该清理掉树中不再使用的部分。.clear()方法的keep_tail=True参数保证当前节点之后的tail文本不会被改动。强烈不建议修改任何解析器可能尚未完全阅读的内容。

>>> some_file_like = BytesIO(
...     b"<root><a><b>data</b></a><a><b/></a></root>")

>>> for event, element in etree.iterparse(some_file_like):
...     if element.tag == 'b':
...         print(element.text)
...     elif element.tag == 'a':
...         print("** cleaning up the subtree")
...         element.clear(keep_tail=True)
data
** cleaning up the subtree
None
** cleaning up the subtree

iterparse()非常重要的使用例在于解析大型 XML 生成文件(如数据库输出)。这些 XML 格式通常仅有一份主要的数据元素项,位于根节点下且重复数千次。最佳做法是令 lxml.etree 执行树的创建工作,仅精确截取所需的节点,使用一般的树 API 提取数据。

>>> xml_file = BytesIO(b'''\
... <root>
...   <a><b>ABC</b><c>abc</c></a>
...   <a><b>MORE DATA</b><c>more data</c></a>
...   <a><b>XYZ</b><c>xyz</c></a>
... </root>''')

>>> for _, element in etree.iterparse(xml_file, tag='a'):
...     print('%s -- %s' % (element.findtext('b'), element[1].text))
...     element.clear(keep_tail=True)
ABC -- abc
MORE DATA -- more data
XYZ -- xyz

若出于某些原因并不要求建立树,则使用 lxml.etree 的目标解析器接口。通过调用目标对象的方法创建 SAX 样事件。通过控制这些方法中的部分或全部,你能控制生成什么事件:

>>> class ParserTarget:
...     events = []
...     close_count = 0
...     def start(self, tag, attrib):
...         self.events.append(("start", tag, attrib))
...     def close(self):
...         events, self.events = self.events, []
...         self.close_count += 1
...         return events

>>> parser_target = ParserTarget()

>>> parser = etree.XMLParser(target=parser_target)
>>> events = etree.fromstring('<root test="true"/>', parser)

>>> print(parser_target.close_count)
1

>>> for event in events:
...     print('event: %s - tag: %s' % (event[0], event[1]))
...     for attr, value in event[2].items():
...         print(' * %s = %s' % (attr, value))
event: start - tag: root
 * test = true

你能任意复用解析器及其目标,所以你该当心.close()方法是否真的将目标重置为可用状态(错误的场景也是!)。

>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
2
>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
3
>>> events = etree.fromstring('<root test="true"/>', parser)
>>> print(parser_target.close_count)
4

>>> for event in events:
...     print('event: %s - tag: %s' % (event[0], event[1]))
...     for attr, value in event[2].items():
...         print(' * %s = %s' % (attr, value))
event: start - tag: root
 * test = true

命名空间

ELementTree API 尽量避免命名空间前缀,而采用真实命名空间 URI 代替:

>>> xhtml = etree.Element("{http://www.w3.org/1999/xhtml}html")
>>> body = etree.SubElement(xhtml, "{http://www.w3.org/1999/xhtml}body")
>>> body.text = "Hello World"

>>> print(etree.tostring(xhtml, pretty_print=True))
<html:html xmlns:html="http://www.w3.org/1999/xhtml">
  <html:body>Hello World</html:body>
</html:html>

ElementTree 使用的记号最初来自 James Clark。其主要优点在于为标签提供通用的限定名,放弃任何可能在文档中未被使用或定义的前缀。通过消除前缀的歧义性,命名空间识别代码更加清晰和简洁。

正如上例所示,前缀只会在序列化结果时发挥重要作用。然而,上述代码由于命名空间名称过大而显得冗长,且重复输入容易造成错误。因此,将命名空间 URI 存储于全局变量是常见做法。为了应用前缀引导序列化,你也能向Element工厂方法传递(比如指向定义)默认命名空间的映射:

>>> XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
>>> XHTML = "{%s}" % XHTML_NAMESPACE

>>> NSMAP = {None : XHTML_NAMESPACE} # the default namespace (no prefix)

>>> xhtml = etree.Element(XHTML + "html", nsmap=NSMAP) # lxml only!
>>> body = etree.SubElement(xhtml, XHTML + "body")
>>> body.text = "Hello World"

>>> print(etree.tostring(xhtml, pretty_print=True))
<html xmlns="http://www.w3.org/1999/xhtml">
  <body>Hello World</body>
</html>

你也能使用QName辅助类建造或分割限定标签名:

>>> tag = etree.QName('http://www.w3.org/1999/xhtml', 'html')
>>> print(tag.localname)
html
>>> print(tag.namespace)
http://www.w3.org/1999/xhtml
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}html

>>> tag = etree.QName('{http://www.w3.org/1999/xhtml}html')
>>> print(tag.localname)
html
>>> print(tag.namespace)
http://www.w3.org/1999/xhtml

>>> root = etree.Element('{http://www.w3.org/1999/xhtml}html')
>>> tag = etree.QName(root)
>>> print(tag.localname)
html

>>> tag = etree.QName(root, 'script')
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}script
>>> tag = etree.QName('{http://www.w3.org/1999/xhtml}html', 'script')
>>> print(tag.text)
{http://www.w3.org/1999/xhtml}script

lxml.etree 允许通过.nsmap属性查询当前为节点定义的命名空间:

>>> xhtml.nsmap
{None: 'http://www.w3.org/1999/xhtml'}

然而,该属性不仅包含Element自身的前缀,还有Element所在的树结构中全部已知的前缀:

>>> root = etree.Element('root', nsmap={'a': 'http://a.b/c'})
>>> child = etree.SubElement(root, 'child',
...                          nsmap={'b': 'http://b.c/d'})
>>> len(root.nsmap)
1
>>> len(child.nsmap)
2
>>> child.nsmap['a']
'http://a.b/c'
>>> child.nsmap['b']
'http://b.c/d'

因此,修改返回的字典不会对Element造成任何有意义的影响,所有的修改都被忽略。

属性的命名空间工作原理类似,但在 2.3 版本中,lxml.etree 属性使用命名空间前缀声明。这是因为非前缀属性名并不被 XML 命名空间规范(6.2)认为处于命名空间中。除非这些非前缀属性名出现在已从属的命名空间内的节点中,否则可能在序列解析的往返过程中失去命名空间。

>>> body.set(XHTML + "bgcolor", "#CCFFAA")

>>> print(etree.tostring(xhtml, pretty_print=True))
<html xmlns="http://www.w3.org/1999/xhtml">
  <body xmlns:html="http://www.w3.org/1999/xhtml" html:bgcolor="#CCFFAA">Hello World</body>
</html>

>>> print(body.get("bgcolor"))
None
>>> body.get(XHTML + "bgcolor")
'#CCFFAA'

你也能在 XPath 中使用完整限定名:

>>> find_xhtml_body = etree.ETXPath(      # lxml only !
...     "//{%s}body" % XHTML_NAMESPACE)
>>> results = find_xhtml_body(xhtml)

>>> print(results[0].tag)
{http://www.w3.org/1999/xhtml}body

为了方便,你可以在 lxml.etree 的所有迭代器中为标签名和命名空间使用" * "通配符。

>>> for el in xhtml.iter('*'): print(el.tag)   # any element
{http://www.w3.org/1999/xhtml}html
{http://www.w3.org/1999/xhtml}body
>>> for el in xhtml.iter('{http://www.w3.org/1999/xhtml}*'): print(el.tag)
{http://www.w3.org/1999/xhtml}html
{http://www.w3.org/1999/xhtml}body
>>> for el in xhtml.iter('{*}body'): print(el.tag)
{http://www.w3.org/1999/xhtml}body

为寻找没有命名空间的节点,或使用通用标签名,或显式地提供空的命名空间:

>>> [ el.tag for el in xhtml.iter('{http://www.w3.org/1999/xhtml}body') ]
['{http://www.w3.org/1999/xhtml}body']
>>> [ el.tag for el in xhtml.iter('body') ]
[]
>>> [ el.tag for el in xhtml.iter('{}body') ]
[]
>>> [ el.tag for el in xhtml.iter('{}*') ]
[]

E-工厂

E-工厂提供了简洁紧凑的语法以生成 XMLHTML

>>> from lxml.builder import E

>>> def CLASS(*args): # class is a reserved word in Python
...     return {"class":' '.join(args)}

>>> html = page = (
...   E.html(       # create an Element called "html"
...     E.head(
...       E.title("This is a sample document")
...     ),
...     E.body(
...       E.h1("Hello!", CLASS("title")),
...       E.p("This is a paragraph with ", E.b("bold"), " text in it!"),
...       E.p("This is another paragraph, with a", "\n      ",
...         E.a("link", href="http://www.python.org"), "."),
...       E.p("Here are some reserved characters: <spam&egg>."),
...       etree.XML("<p>And finally an embedded XHTML fragment.</p>"),
...     )
...   )
... )

>>> print(etree.tostring(page, pretty_print=True))
<html>
  <head>
    <title>This is a sample document</title>
  </head>
  <body>
    <h1 class="title">Hello!</h1>
    <p>This is a paragraph with <b>bold</b> text in it!</p>
    <p>This is another paragraph, with a
      <a href="http://www.python.org">link</a>.</p>
    <p>Here are some reserved characters: &lt;spam&amp;egg&gt;.</p>
    <p>And finally an embedded XHTML fragment.</p>
  </body>
</html>

基于属性存取建立节点便于为 XML 语言提供简单的词汇表:

>>> from lxml.builder import ElementMaker # lxml only !

>>> E = ElementMaker(namespace="http://my.de/fault/namespace",
...                  nsmap={'p' : "http://my.de/fault/namespace"})

>>> DOC = E.doc
>>> TITLE = E.title
>>> SECTION = E.section
>>> PAR = E.par

>>> my_doc = DOC(
...   TITLE("The dog and the hog"),
...   SECTION(
...     TITLE("The dog"),
...     PAR("Once upon a time, ..."),
...     PAR("And then ...")
...   ),
...   SECTION(
...     TITLE("The hog"),
...     PAR("Sooner or later ...")
...   )
... )

>>> print(etree.tostring(my_doc, pretty_print=True))
<p:doc xmlns:p="http://my.de/fault/namespace">
  <p:title>The dog and the hog</p:title>
  <p:section>
    <p:title>The dog</p:title>
    <p:par>Once upon a time, ...</p:par>
    <p:par>And then ...</p:par>
  </p:section>
  <p:section>
    <p:title>The hog</p:title>
    <p:par>Sooner or later ...</p:par>
  </p:section>
</p:doc>

类似的实例之一是 lxml.html.builder 模块,为 HTML 提供词汇表。

处理多种命名空间时,好的做法是为每个命名空间 URI 定义一个ElementMaker。而且,请认真回顾以上的用例如何通过命名常量预定义标签创建器,这简化了将一个命名空间的所有标签声明放入 Python 模块、并从模块中导入或使用标签名常量的过程。这种做法也避免了键入或意外丢失命名空间的常见小错误。

ElementPath

ElementTree库提供一款简单的仿 XPath 路径语言,称为 ElementPath。主要的区别在于你能在 ElementPath 表达式中使用{namespace}tag记号。然而,该语言不支持值比较和函数的高级特性。

除了完全的 XPath 实现lxml.etree 还以 ElementTree 的相同方式支持 ElementPath 语言,甚至使用(几乎)相同的实现。API 提供了你能在 ElementElementTree 找到的以下四种方法:

  • iterfind() 迭代匹配路径表达式的所有 Element
  • findall() 返回匹配 Element 的列表
  • find() 仅返回初次匹配
  • findtext() 返回初次匹配的.text内容

以下是一些例子:

>>> root = etree.XML("<root><a x='123'>aText<b/><c/><b/></a></root>")

寻找ELement的子节点:

>>> print(root.find("b"))
None
>>> print(root.find("a").tag)
a

整棵树中寻找ELement

>>> print(root.find(".//b").tag)
b
>>> [ b.tag for b in root.iterfind(".//b") ]
['b', 'b']

寻找有特定属性的ELement

>>> print(root.findall(".//a[@x]")[0].tag)
a
>>> print(root.findall(".//a[@y]"))
[]

lxml 3.4 中,新的辅助函数能为Element生成结构化的路径表达式:

>>> tree = etree.ElementTree(root)
>>> a = root[0]
>>> print(tree.getelementpath(a[0]))
a/b[1]
>>> print(tree.getelementpath(a[1]))
a/c
>>> print(tree.getelementpath(a[2]))
a/b[2]
>>> tree.find(tree.getelementpath(a[2])) == a[2]
True

只要树未被修改,路径表达式就代表了给定节点的唯一标识符,能在同一颗树中用于find()节点。相较于 XPathElementPath 的优势在于即使对于使用命名空间的文档也是自包含的(self-contained)。

.iter()方法是在树中使用名称而非路径找到标签的特例。意味着以下命令在成功执行的情况下是等价的:

>>> print(root.find(".//b").tag)
b
>>> print(next(root.iterfind(".//b")).tag)
b
>>> print(next(root.iter("b")).tag)
b

如果没有匹配,.find()方法简单地返回None,然而其他两例会抛出StopIteration异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值