lxml.etree 官方教学文档翻译
文章目录
文档地址:
lxml.etree Tutorial
教学文档
>>> from lxml import etree
>>> etree.LXML_VERSION
(4, 5, 1, 0)
Element
类
Element
是 ElementTree 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.3 与 lxml 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.3 及 Python 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.etree 的Element
最多只能有一个父节点,该父节点能被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
的兄弟节点可以作为next
和previous
节点被访问:
>>> 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/>
节点被文本环绕,这常被称为文档样式型或混合内容 XML,Element
类通过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>'
text
与tail
属性足以表示 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/>'
若你还希望仅读取文本而忽略标签,以正确的顺序递归地串联text
与tail
文本,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
事实上,这一原则仅适用于 XPath 的text()
函数,而对 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
、注释Comment
和Entity
实例。
如果你希望只返回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: ê - ê
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)
ê
记住传递通配符“ * ”标签名同样会yield
所有Element
节点(仅Element
节点)。
lxml.etree 中,在各个方向(如子节点、父节点、兄弟节点)提供了更深入的迭代器。
序列化 Serialisation
序列化常使用tostring()
函数返回字符串,或使用ElementTree.write()
方法写入文件、文件样对象、甚至 URL(通过 FTP PUT
或 HTTP 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.0 与 ElementTree 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'
序列化为 Python 的 unicode 字符串而非 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)
方法的文件样对象,每次调用返回字符串字节 - 文件名字符串
- HTTP 或 FTP URL 字符串
注意到传递文件名或 URL 通常比传递打开的文件或文件样对象更快。然而,libxml2
的 HTTP/FTP 客户端相当简单,因此 HTTP 认证之类的事务需要专门的 URL 请求库,如urllib2
或requests
。这些库通常提供文件样对象作为结果,你能在响应流进入的同时进行解析。
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-工厂提供了简洁紧凑的语法以生成 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( # 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: <spam&egg>.</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 提供了你能在 Element 和 ElementTree 找到的以下四种方法:
iterfind()
迭代匹配路径表达式的所有 Elementfindall()
返回匹配 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()
节点。相较于 XPath ,ElementPath 的优势在于即使对于使用命名空间的文档也是自包含的(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
异常。