用 ElementTree 在 Python 中解析 XML

原文: 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:準確來說,樹的根元素仍然存活。在某些情況下根結點非常大,你也可以丟棄它,但那需要多一點點代碼。

發佈了26 篇原創文章 · 獲贊 13 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章