lxml官方入门教程(The lxml.etree Tutorial)翻译

lxml官方入门教程(The lxml.etree Tutorial)翻译

说明:

  • 首次发表日期:2024-09-05
  • 官方教程链接: https://lxml.de/tutorial.html
  • 使用KIMI和豆包机翻
  • 水平有限,如有错误请不吝指出

这是一个关于使用lxml.etree处理XML的教程。它简要概述了ElementTree API的主要概念,以及一些简单的增强功能,这些功能可以让您作为程序员的生活更轻松。

有关API的完整参考,请查看生成的API文档

导入lxml.etree的常见方式如下:

from lxml import etree

如果你的代码仅使用ElementTree API,并且不依赖于lxml.etree任何的特有功能,您还可以使用以下导入链来回退到Python标准库中的ElementTree:

try:
    from lxml import etree
    print("running with lxml.etree")
except ImportError:
    import xml.etree.ElementTree as etree
    print("running with Python's xml.etree.ElementTree")

为了帮助编写可移植代码,本教程在示例中明确指出了所呈现API的哪一部分是lxml.etree对原始ElementTree API的扩展。

The Element class

元素(Element)是ElementTree API的主要容器对象。大部分XML树功能都是通过这个类访问的。元素(Elements)可以通过Element factory轻松创建:

root = etree.Element("root")

元素的XML标签名称可以通过tag属性访问:

print(root.tag)

元素在XML树结构中组织。要创建子元素并将它们添加到父元素,您可以使用append()方法:

root.append(etree.Element("child"))

然而,这种情况非常常见,因此有一个更简短且效率更高的方法来实现这一点:SubElement工厂。它接受与Element工厂相同的参数,但另外需要将父元素作为第一个参数:

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

要确认这确实是XML,您可以序列化您创建的树:

etree.tostring(root)
b'<root><child1/><child2/><child3/></root>'

我们将创建一个小型辅助函数,为我们美观地打印XML:

def prettyprint(element, **kwargs):
    xml = etree.tostring(element, pretty_print=True, **kwargs)
    print(xml.decode(), end='')
prettyprint(root)
<root>
  <child1/>
  <child2/>
  <child3/>
</root>

Elements are lists

为了便于直接访问这些子元素,元素尽可能地模仿了普通Python列表的行为:

>>> 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:
...     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

在ElementTree 1.3和lxml 2.0之前,您还可以检查元素的真值,以查看它是否有子元素,即查看子元素列表是否为空:

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

这种做法不再被支持,因为人们倾向于期望“某物”(something)evaluates为True,并期望元素(Elements)是“某物”,无论它们是否有子元素。因此,许多用户发现,任何元素在像上面的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")

在另一个重要的场景下,lxml中元素(从2.0及以后版本)的行为与列表(lists)的行为以及原始ElementTree(1.3版本之前或Python 2.7/3.2之前)的行为有所不同:

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中,单个元素对象可以位于任何数量的树中的任何位置,这允许进行与列表相同的复制操作。明显的不足是,对这样的元素进行的修改将应用于它在树中出现的所有位置,这可能是也可能不是预期的。

备注:在lxml中,上述赋值操作会移动元素,与lists和原始的ElementTree中不同。

这种差异的好处是,在lxml.etree中的一个元素总是恰好有一个父元素,这可以通过getparent()方法查询。这在原始的ElementTree中是不支持的。

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

如果您想将元素复制到lxml.etree中的不同位置,请考虑使用Python标准库中的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']

元素的兄弟(或邻居)作为下一个和上一个元素进行访问:

root[0] is root[1].preprevious()  # lxml.etree only!
# True
root[1] is root[0].getnext() # lxml.etree only!

Elements carry attributes as a dict

XML元素支持属性(attributes)。您可以直接在Element工厂中创建它们:

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

属性只是无序的名称-值对,因此通过元素的类似字典的接口处理它们非常方便:

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是一个由元素本身支持(backed)的类似字典的对象。这意味着对元素的任何更改都会反映在attrib中,反之亦然。这也意味着只要XML树有一个元素的attrib在使用中,XML树就会在内存中保持活动状态。要获取一个不依赖于XML树的属性的独立快照,将其复制到一个字典中:

d = dict(root.attrib)
sorted(d.items())
# ('hello', 'Guten Tag'), ('interesting', 'totally')]

Elements contain text

元素可以包含文本:

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

print(root.text)
# TEXT

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

在许多XML文档(以数据为中心的文档)中,这是唯一可以找到文本的地方。它被树层次结构最底层的一个叶子标签封装。

然而,如果XML用于标记文本文档,如(X)HTML,文本也可以出现在不同元素之间,就在树的中间:

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

在这里,<br/> 标签被文本包围。这通常被称为文档样式或混合内容XML。元素通过它们的tail属性支持这一点。它包含直接跟随元素的文本,直到XML树中的下一个元素:

>>> 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>'

.text.tail 这两个属性足以表示XML文档中的任何文本内容。这样,ElementTree API 除了 “Element” 类之外不需要任何特殊的文本节点,这些节点往往经常会常造成阻碍(正如你可能从传统 DOM API 中了解到的那样)。

然而,也有一些情况下,尾随(tail)文本也会碍事。例如,当您序列化树中的一个元素时,您并不总是希望其尾随文本出现在结果中(尽管您仍然希望包含其子元素的尾部文本)。为此,tostring() 函数接受关键字参数 with_tail

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

如果你只想读取文本,即不包含任何中间标签,你必须以正确的顺序递归地连接所有的文本和尾部属性。同样,“tostring()” 函数可以提供帮助,这次使用 “method” 关键字。

>>> etree.tostring(html, method="text")
b'TEXTTAIL'

Using XPath to find text

提取树文本内容的另一种方式是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

您还可以找出它是普通文本内容还是尾随文本:

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

虽然这对text()函数的结果有效,但lxml不会告诉您由XPath函数string()或concat()构造的字符串值的来源:

>>> stringify = etree.XPath("string()")
>>> print(stringify(html))
TEXTTAIL
>>> print(stringify(html).getparent())
None

Tree iteration

对于上述这样的问题,当你想要递归地遍历树并对其元素进行一些操作时,树迭代(tree iteration)是一个非常方便的解决方案。元素(Elements)为此提供了一个树迭代器。它按照文档顺序生成元素,即与将树序列化为XML时其标签出现的顺序一致。

>>> 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"

>>> prettyprint(root)
<root>
  <child>Child 1</child>
  <child>Child 2</child>
  <another>Child 3</another>
</root>

>>> for element in root.iter():
...     print(f"{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(f"{element.tag} - {element.text}")
child - Child 1
child - Child 2

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

默认情况下,迭代会生成树中的所有节点,包括ProcessingInstructions、Comments和Entity实例。如果您想确保只返回Element对象,可以将Element工厂作为标签参数传递:

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

>>> for element in root.iter():
...     if isinstance(element.tag, str):
...         print(f"{element.tag} - {element.text}")
...     else:
...         print(f"SPECIAL: {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(f"{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;

请注意,传递通配符“*”作为标签名也将生成所有的Element节点(并且只有元素节点)。

for element in root.iter(tag="*"):
    if isinstance(element.tag, str):
        print(f"element.tag - {element.text}")
    else:
        print(f"SPECIAL: {element} - {element.text}")
element.tag - None
element.tag - Child 1
element.tag - Child 2
element.tag - Child 3

lxml.etree中,elements为树中的所有方向提供了进一步的迭代器:子节点(iterchildren())、父节点(或者更确切地说是祖先节点)(iterancestors())和兄弟节点(itersiblings())。

Serialisation

序列化通常使用tostring()函数返回字符串,或者使用ElementTree.write()方法写入文件、类文件对象(file-like object)或URL(通过FTP PUT或HTTP POST)。这两个调用都接受相同的关键字参数,如pretty_print用于格式化输出,或者encoding用于选择除纯ASCII之外的特定输出编码:

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

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

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

>>> latin1_bytesstring = etree.tostring(root, encoding='iso8859-1')
>>> print(latin1_bytesstring.decode('iso8859-1'), end='')
<?xml version='1.0' encoding='iso8859-1'?>
<root><a><b/></a></root>

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

请注意,美观打印(pretty_print)会在末尾附加一个新行。因此,我们在这里使用end=''选项,以防止print()函数添加另一个换行符。

为了在序列化之前对美观打印(pretty_print)进行更细粒度的控制,您可以使用indent()函数(在lxml 4.5中添加)在树中添加空白缩进:

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

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

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

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

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

在lxml 2.0及更高版本以及xml.etree中,序列化函数不仅可以进行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>'

>>> prettyprint(root, method='html')
<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 = 'Wörld'

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

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

在这里,将序列化目标设为Python文本字符串(text string)而不是字节字符串(byte string)可能会很方便。只需将'unicode'作为编码传递:

>>> etree.tostring(root, encoding='unicode', method='text')
'HelloWörld'
>>> etree.tostring(root, encoding='unicode')
'<html><head/><body><p>Hello<br/>Wörld</p></body></html>'

W3C有一篇关于Unicode字符集和字符编码的好文章: https://www.w3.org/International/tutorials/tutorial-char-enc/

The ElementTree class

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">

当您调用parse()函数解析文件或类文件对象(file-like object)时(见下文的解析部分),您得到的也是一个ElementTree。

一个重要的不同之处在于,ElementTree类序列化为一个完整的文档,而不是单个Element。这包括顶级(top-level)处理指令和注释,以及文档中的DOCTYPE和其他DTD内容:

>>> prettyprint(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实现以及直到1.3.3版本的lxml中,输出看起来与仅序列化根元素时相同:

>>> prettyprint(tree.getroot())
<root>
  <a>parsnips</a>
</root>

在lxml 1.3.4中,这种序列化行为发生了变化。以前,树在没有DTD内容的情况下被序列化,这使得lxml在输入输出循环中丢失了DTD信息。

Parsing from strings and files

lxml.etree支持以多种方式解析XML,并且可以从所有重要的源解析,即字符串、文件、URL(http/ftp)和类文件对象(file-like object)。主要的解析函数是fromstring()parse(),都以源作为第一个参数调用。默认情况下,它们使用标准解析器,但您总是可以作为第二个参数传递不同的解析器。

The fromstring() function

fromstring() 函数是解析字符串的最简单方法:

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

>>> root = etree.fromstring(some_xml_data)
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'
print(type(root))
# <class 'lxml.etree._Element'>

The XML() function

XML()函数的行为类似于fromstring()函数,但通常用于将XML字面量直接写入源代码:

>>> root = etree.XML("<root>data</root>")
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'
print(type(root))
# <class 'lxml.etree._Element'>

还有一个相应的函数HTML()用于HTML字面量。

>>> root = etree.HTML("<p>data</p>")
>>> etree.tostring(root)
b'<html><body><p>data</p></body></html>'
print(type(root))
# <class 'lxml.etree._Element'>

The parse() function

parse()函数用于从文件和类文件对象(file-like object)解析。

作为此类类文件对象的一个例子,以下代码使用BytesIO类从字符串而不是外部文件中读取。然而,在现实生活中,您显然会避免这样做,而是使用像上面提到的fromstring()这样的字符串解析函数。

>>> 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对象:

print(type(tree))
# <class 'lxml.etree._ElementTree'>
>>> root = tree.getroot()
>>> print(root.tag)
root
>>> etree.tostring(root)
b'<root>data</root>'

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

parse()函数支持以下任何来源:

  • 一个打开的文件对象(确保以二进制模式打开)
  • 一个具有.read(byte_count)方法的类文件对象,每次调用都返回一个字节字符串
  • 一个文件名字符串
  • 一个HTTP或FTP URL字符串

请注意,传递文件名或URL通常比传递打开的文件或类文件对象更快。然而,libxml2中的HTTP/FTP客户端相当简单,因此像HTTP认证这样的事情需要一个专门的URL请求库,例如urllib2或requests。这些库通常提供一个类文件对象作为结果,您可以在响应流式传输时从中解析。

Parser objects

默认情况下,lxml.etree使用具有默认设置的标准解析器。如果您想配置解析器,可以创建一个新实例:

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

这创建了一个解析器,在解析时删除标签之间的空白文本,这可以减少树的大小,并避免在您知道空白内容对您的数据没有意义时出现悬挂的尾随文本。例如:

>>> 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) 以了解有关可用解析器选项的信息。

help(etree.XMLParser)

Incremental parsing

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>'

第二种方法是通过parser提供的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 这样的框架中,或者每当数据缓慢地或以块的形式到来,并且你在等待下一块数据时想要做其他事情的时候。

在调用close()方法(或解析器引发异常)之后,您可以通过再次调用其feed()方法来重用解析器:

parser.feed("<root/>")
root = parser.close()
etree.tostring(root)
b'<root/>'

Event-driven parsing

有时,您从文档中所需的只是树内部深处的一小部分,因此将整个树解析到内存中、遍历它然后丢弃它可能会有太多的开销。lxml.etree通过两种事件驱动的解析器接口支持这种用例,一种在构建树时生成解析器事件(iterparse),另一种根本不构建树,而是以类似SAX的方式在目标对象上调用反馈方法。

这里有一个简单的iterparse()示例:

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

for event, element in etree.iterparse(some_file_like):
    print(f"{event}, {element.tag:>4}, {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(f"{event:>5}, {element.tag:>4}, {element.text}")
start, root, None
start,    a, data
  end,    a, data
  end, root, None

请注意,在接收start事件时,元素的文本、尾随文本和子元素不一定已经存在。只有end事件保证了元素已经被完全解析。

它还允许您使用.clear()方法或修改元素的内容以节省内存。因此,如果您解析了一个大的树,并且您希望保持内存使用量小,您应该清理不再需要的树的部分。.clear()方法的keep_tail=True参数确保当前元素后面的(尾随)文本内容不会被触动。强烈不建议修改解析器可能尚未完全读取的任何内容。

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文件,例如数据库转储(database dumps)。最常见的情况是,这些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的目标解析器接口(target parser interface)。它通过调用目标对象的方法创建类似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
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(f'event: {event[0]} - tag: {event[1]}')
...     for attr, value in event[2].items():
...         print(f' * {attr} = {value}')
event: start - tag: root
 * test = true

Namespaces

只要有可能,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"

>>> prettyprint(xhtml)
<html:html xmlns:html="http://www.w3.org/1999/xhtml">
  <html:body>Hello World</html:body>
</html:html>

ElementTree使用的表示法最初由James Clark提出。它的主要优点是为标签提供了一个通用限定名称(universally qualified name),无论文档中可能已经使用或定义的任何前缀。通过将前缀的间接性(indirection of prefixes)移开,它使命名空间感知的代码更加清晰,更容易正确处理。

正如您从示例中看到的,前缀只在序列化结果时变得重要。然而,由于命名空间名称较长,上述代码看起来有些冗长。而且,一遍又一遍地重新键入或复制字符串容易出错。因此,通常的做法是将命名空间URI存储在全局变量中。为了调整(adapt)用于序列化的命名空间前缀,你也可以将一个映射传递给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"

>>> prettyprint(xhtml)
<html xmlns="http://www.w3.org/1999/xhtml">
  <body>Hello World</body>
</html>

你也可以使用QName辅助类来构建或拆分限定的标签名称(qualified tag names)。

>>> 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'}

请注意,这包括在元素的上下文中已知的所有前缀,而不仅仅是它自己定义的那些。

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

因此,修改返回的字典对Element(元素)没有任何有意义的影响。对它的任何更改都会被忽略。

属性(attributes)上的命名空间工作方式类似,但自2.3版本起,lxml.etree将确保属性使用带有前缀的命名空间声明。这是因为XML命名空间规范(第6.2节)认为未加前缀的属性名称不处于任何命名空间中,因此即使它们出现在命名空间元素中,它们在序列化-解析循环中可能会丢失其命名空间。

body.set(XHTML + "bgcolor", "#CCFFAA")
prettyprint(xhtml)
<html xmlns="http://www.w3.org/1999/xhtml">
  <body xmlns:html="http://www.w3.org/1999/xhtml" html:bgcolor="#CCFFAA">Hello World</body>
</html>
# XML命名空间规范认为未加前缀的属性名称不处于任何命名空间中,所以返回None
print(body.get("bgcolor"))
None
# 使用加上前缀的属性名称
body.get(XHTML + "bgcolor")
'#CCFFAA'

您还可以使用完全限定的名称(fully qualified names)来使用XPath:

# 先回顾一下xhtml的内容
print(etree.tostring(xhtml).decode())
<html xmlns="http://www.w3.org/1999/xhtml"><body xmlns:html="http://www.w3.org/1999/xhtml" html:bgcolor="#CCFFAA" bgcolor="#CCFFAA">Hello World</body></html>
>>> 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('{}*') ]
[]

The E-factory

E-factory提供了一种简单紧凑的语法,用于生成XML和HTML:

from lxml.builder import E

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

html = page = (
    E.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>"),
        )
    )
)

prettyprint(page)
<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 ...")
    )
)

prettyprint(my_doc)
<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。再次注意,上述示例如何在命名常量中预定义了标签构建器(tag builders)。这使得将一个命名空间的所有标签声明放入一个Python模块,并从那里导入/使用标签名称常量变得容易。这避免了诸如拼写错误或意外遗漏命名空间之类的陷阱。

ElementPath

ElementTree库附带了一个简单的类似XPath的路径语言,称为ElementPath。主要区别在于您可以在ElementPath表达式中使用{namespace}tag表示法。然而,高级特性如值比较和函数是不可用的。

除了完整的XPath实现,lxml.etree以与ElementTree相同的方式支持ElementPath语言,甚至使用(几乎)相同的实现。API在这里提供了四种方法,您可以在Elements和ElementTrees上找到这些方法:

  • iterfind() 遍历所有匹配路径表达式(path expression)的元素。
  • findall() 返回匹配元素的列表。
  • find() 高效地仅返回第一个匹配项。
  • findtext() 返回第一个匹配项的 .text 内容。

这里有一些示例:

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

查找元素的子元素:

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

在树中查找元素:

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

查找具有特定属性的元素:

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

lxml3.4 版本中,有一个新的辅助函数用于为一个Element生成结构化的ElementPath表达式。

>>> 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()找到它。与XPath相比,ElementPath表达式的优势在于即使对于使用命名空间的文档,它们也是自包含的。

.iter()方法是一个特例,它仅通过名称在树中查找特定标签,而不是基于路径。这意味着在成功的情况下,以下命令是等效的:

>>> print(root.find(".//b").tag)
b
>>> print(next(root.iterfind(".//b")).tag)
b
>>> print(next(root.iter("b")).tag)
b
posted @ 2024-09-05 16:39  shizidushu  阅读(167)  评论(0编辑  收藏  举报