lxml官方入門教程(The lxml.etree Tutorial)翻譯

shizidushu發表於2024-09-05

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

相關文章