原文: http://eli.thegreenplace.net/2012/03/15/processing-xml-in-python-with-elementtree/
譯者: TheLover_Z
當你需要解析和處理 XML 的時候,Python 表現出了它 “batteries included” 的一面。 標準庫 中大量可用的模塊和工具足以應對 Python 或者是 XML 的新手。
幾個月前在 Python 核心開發者之間發生了一場 有趣的討論 ,他們討論了 Python 下可用的 XML 處理工具的優點,還有如何將它們最好的展示給用戶看。這篇文章是我本人的拙作,我打算講講哪些工具比較好用還有爲什麼它們好用,當然,這篇文章也可以當作一個如何使用的基礎教程來看。
這篇文章所使用的代碼基於 Python 2.7,你稍微改動一下就可以在 Python 3.x 上面使用了。
應該使用哪個 XML 庫?
Python 有非常非常多的工具來處理 XML。在這個部分我想對 Python 所提供的包進行一個簡單的瀏覽,並且解釋爲什麼 ElementTree 是你最應該用的那一個。
xml.dom.* 模塊 - 是 W3C DOM API 的實現。如果你有處理 DOM API 的需要,那麼這個模塊適合你。注意:在 xml.dom 包裏面有許多模塊,注意它們之間的不同。
xml.sax.* 模塊 - 是 SAX API 的實現。這個模塊犧牲了便捷性來換取速度和內存佔用。SAX 是一個基於事件的 API,這就意味着它可以“在空中”(on the fly)處理龐大數量的的文檔,不用完全加載進內存(見註釋1)。
xml.parser.expat - 是一個直接的,低級一點的基於 C 的 expat 的語法分析器(見註釋2)。 expat 接口基於事件反饋,有點像 SAX 但又不太像,因爲它的接口並不是完全規範於 expat 庫的。
最後,我們來看看 xml.etree.ElementTree (以下簡稱 ET)。它提供了輕量級的 Python 式的 API ,它由一個 C 實現來提供。相對於 DOM 來說,ET 快了很多(見註釋3)而且有很多令人愉悅的 API 可以使用。相對於 SAX 來說,ET 也有 ET.iterparse 提供了 “在空中” 的處理方式,沒有必要加載整個文檔到內存。ET 的性能的平均值和 SAX 差不多,但是 API 的效率更高一點而且使用起來很方便。我一會兒會給你們看演示。
我的建議 是儘可能的使用 ET 來處理 XML ,除非你有什麼非常特別的需要。
ElementTree - 一個 API ,兩種實現
ElementTree 生來就是爲了處理 XML ,它在 Python 標準庫中有兩種實現。一種是純 Python 實現例如 xml.etree.ElementTree ,另外一種是速度快一點的 xml.etree.cElementTree 。你要記住: 儘量使用 C 語言實現的那種,因爲它速度更快,而且消耗的內存更少。如果你的電腦上沒有 _elementtree (見註釋4) 那麼你需要這樣做:
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
這是一個讓 Python 不同的庫使用相同 API 的一個比較常用的辦法。還是那句話,你的編譯環境和別人的很可能不一樣,所以這樣做可以防止一些莫名其妙的小問題。注意:從 Python 3.3 開始,你沒有必要這麼做了,因爲 ElementTree 模塊會自動尋找可用的 C 庫來加快速度。所以只需要 import xml.etree.ElementTree 就可以了。但是在 3.3 正式推出之前,你最好還是使用我上面提供的那段代碼。
將 XML 解析爲樹的形式
我們來講點基礎的。XML 是一種分級的數據形式,所以最自然的表示方法是將它表示爲一棵樹。ET 有兩個對象來實現這個目的 - ElementTree 將整個 XML 解析爲一棵樹, Element 將單個結點解析爲樹。如果是整個文檔級別的操作(比如說讀,寫,找到一些有趣的元素)通常用 ElementTree 。單個 XML 元素和它的子元素通常用 Element 。下面的例子能說明我剛纔囉嗦的一大堆。(見註釋5)
我們用這個 XML 文件來做例子:
<?xml version="1.0"?>
<doc>
<branch name="testing" hash="1cdf045c">
text,source
</branch>
<branch name="release01" hash="f200013e">
<sub-branch name="subrelease01">
xml,sgml
</sub-branch>
</branch>
<branch name="invalid">
</branch>
</doc>
讓我們加載並且解析這個 XML :
>>> import xml.etree.cElementTree as ET
>>> tree = ET.ElementTree(file='doc1.xml')
然後抓根結點元素:
>>> tree.getroot()
<Element 'doc' at 0x11eb780>
和預期一樣,root 是一個 Element 元素。我們可以來看看:
>>> root = tree.getroot()
>>> root.tag, root.attrib
('doc', {})
看吧,根元素沒有任何狀態(見註釋6)。就像任何 Element 一樣,它可以找到自己的子結點:
>>> for child_of_root in root:
... print child_of_root.tag, child_of_root.attrib
...
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
branch {'name': 'invalid'}
我們也可以進入一個指定的子結點:
>>> root[0].tag, root[0].text
('branch', '\n text,source\n ')
找到我們感興趣的元素
從上面的例子我們可以輕而易舉的看到,我們可以用一個簡單的遞歸獲取 XML 中的任何元素。然而,因爲這個操作比較普遍,ET 提供了一些有用的工具來簡化操作.
Element 對象有一個 iter 方法可以對子結點進行深度優先遍歷。 ElementTree 對象也有 iter 方法來提供便利。
>>> for elem in tree.iter():
... print elem.tag, elem.attrib
...
doc {}
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
sub-branch {'name': 'subrelease01'}
branch {'name': 'invalid'}
遍歷所有的元素,然後檢驗有沒有你想要的。ET 可以讓這個過程更便捷。 iter 方法接受一個標籤名字,然後只遍歷那些有指定標籤的元素:
>>> for elem in tree.iter(tag='branch'):
... print elem.tag, elem.attrib
...
branch {'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
branch {'name': 'invalid'}
來自 XPath 的幫助
爲了尋找我們感興趣的元素,一個更加有效的辦法是使用 XPath 支持。 Element 有一些關於尋找的方法可以接受 XPath 作爲參數。 find 返回第一個匹配的子元素, findall 以列表的形式返回所有匹配的子元素, iterfind 爲所有匹配項提供迭代器。這些方法在 ElementTree 裏面也有。
給出一個例子:
>>> for elem in tree.iterfind('branch/sub-branch'):
... print elem.tag, elem.attrib
...
sub-branch {'name': 'subrelease01'}
這個例子在 branch 下面找到所有標籤爲 sub-branch 的元素。然後給出如何找到所有的 branch 元素,用一個指定 name 的狀態即可:
>>> for elem in tree.iterfind('branch[@name="release01"]'):
... print elem.tag, elem.attrib
...
branch {'hash': 'f200013e', 'name': 'release01'}
想要深入學習 XPath 的話,請看 這裏 。
建立 XML 文檔
ET 提供了建立 XML 文檔和寫入文件的便捷方式。 ElementTree 對象提供了 write 方法。
現在,這兒有兩個常用的寫 XML 文檔的腳本。
修改文檔可以使用 Element 對象的方法:
>>> root = tree.getroot()
>>> del root[2]
>>> root[0].set('foo', 'bar')
>>> for subelem in root:
... print subelem.tag, subelem.attrib
...
branch {'foo': 'bar', 'hash': '1cdf045c', 'name': 'testing'}
branch {'hash': 'f200013e', 'name': 'release01'}
我們在這裏刪除了根元素的第三個子結點,然後爲第一個子結點增加新狀態。然後這個樹可以寫回到文件中。
>>> import sys
>>> tree.write(sys.stdout) # ET.dump can also serve this purpose
<doc>
<branch foo="bar" hash="1cdf045c" name="testing">
text,source
</branch>
<branch hash="f200013e" name="release01">
<sub-branch name="subrelease01">
xml,sgml
</sub-branch>
</branch>
</doc>
注意狀態的順序和原文檔的順序不太一樣。這是因爲 ET 講狀態保存在無序的字典中。語義上來說,XML 並不關心順序。
建立一個全新的元素也很容易。ET 模塊提供了 SubElement 函數來簡化過程:
>>> a = ET.Element('elem')
>>> c = ET.SubElement(a, 'child1')
>>> c.text = "some text"
>>> d = ET.SubElement(a, 'child2')
>>> b = ET.Element('elem_b')
>>> root = ET.Element('root')
>>> root.extend((a, b))
>>> tree = ET.ElementTree(root)
>>> tree.write(sys.stdout)
<root><elem><child1>some text</child1><child2 /></elem><elem_b /></root>
使用 iterparse 來處理 XML 流
就像我在文章一開頭提到的那樣,XML 文檔通常比較大,所以將它們全部讀入內存的庫可能會有點兒小問題。這也是爲什麼我建議使用 SAX API 來替代 DOM 。
我們剛講過如何使用 ET 來將 XML 讀入內存並且處理。但它就不會碰到和 DOM 一樣的內存問題麼?當然會。這也是爲什麼這個包提供一個特殊的工具,用來處理大型文檔,並且解決了內存問題,這個工具叫 iterparse 。
我給大家演示一個 iterparse 如何使用的例子。我用 自動生成 拿到了一個 XML 文檔來進行說明。這只是開頭的一小部分:
<?xml version="1.0" standalone="yes"?>
<site>
<regions>
<africa>
<item id="item0">
<location>United States</location> <!-- Counting locations -->
<quantity>1</quantity>
<name>duteous nine eighteen </name>
<payment>Creditcard</payment>
<description>
<parlist>
[...]
我已經用註釋標出了我要處理的元素,我們用一個簡單的腳本來計數有多少 location 元素並且文本內容爲“Zimbabwe”。這是用 ET.parse 的一個標準的寫法:
tree = ET.parse(sys.argv[2])
count = 0
for elem in tree.iter(tag='location'):
if elem.text == 'Zimbabwe':
count += 1
print count
所有 XML 樹中的元素都會被檢驗。當處理一個大約 100MB 的 XML 文件時,佔用的內存大約是 560MB ,耗時 2.9 秒。
注意:我們並不需要在內存中加載整顆樹。它檢測我們需要的帶特定值的 location 元素。其他元素被丟棄。這是 iterparse 的來源:
count = 0
for event, elem in ET.iterparse(sys.argv[2]):
if event == 'end':
if elem.tag == 'location' and elem.text == 'Zimbabwe':
count += 1
elem.clear() # discard the element
print count
這個循環遍歷 iterparse 事件,檢測“閉合的”(end)事件並且尋找 location 標籤和指定的值。在這裏 elem.clear() 是關鍵 - iterparse 仍然建立一棵樹,只不過不需要全部加載進內存,這樣做可以有效的利用內存空間(見註釋7)。
處理同樣的文件,這個腳本佔用內存只需要僅僅的 7MB ,耗時 2.5 秒。速度的提升歸功於生成樹的時候只遍歷一次。相比較來說, parse 方法首先建立了整個樹,然後再次遍歷來尋找我們需要的元素(所以慢了一點)。
結論
在 Python 衆多處理 XML 的模塊中, ElementTree 真是屌爆了。它將輕量,符合 Python 哲學的 API ,出色的性能完美的結合在了一起。所以說如果要處理 XML ,果斷地使用它吧!
這篇文章簡略地談了談 ET 。我希望這篇拙作可以拋磚引玉。
註釋
註釋1:和 DOM 不一樣,DOM 將整個 XML 加載進內存並且允許隨機訪問任何深度地元素。
註釋2: expat 是一個開源的用於處理 XML 的 C 語言庫。Python 將它融合進自身。
註釋3:Fredrik Lundh,是 ElementTree 的原作者,他提到了一些 基準 。
註釋4:當我提到 _elementtree 的時候,我意思是 C 語言的 cElementTree._elementtree 擴展模塊。
註釋5:確定你手邊有 模塊手冊 然後可以隨時查閱我提到的方法和函數。
註釋6: 狀態 是一個意義太多的術語。Python 對象有狀態,XML 元素也有狀態。希望我能將它們表達的更清楚一點。
註釋7:準確來說,樹的根元素仍然存活。在某些情況下根結點非常大,你也可以丟棄它,但那需要多一點點代碼。