Python可以使用 xml.etree.ElementTree 模塊從簡單的XML文檔中提取數據。 為了演示,假設你想解析Planet Python上的RSS源。下面是相應的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from urllib.request import urlopen from xml.etree.ElementTree import parse # Download the RSS feed and parse it u = urlopen( 'http://planet.python.org/rss20.xml' ) doc = parse(u) # Extract and output tags of interest for item in doc.iterfind( 'channel/item' ): title = item.findtext( 'title' ) date = item.findtext( 'pubDate' ) link = item.findtext( 'link' ) print (title) print (date) print (link) print () |
運行上面的代碼,輸出結果類似這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Steve Holden: Python for Data Analysis Mon, 19 Nov 2012 02:13:51 +0000 http://holdenweb.blogspot.com/2012/11/python-for-data-analysis.html Vasudev Ram: The Python Data model (for v2 and v3) Sun, 18 Nov 2012 22:06:47 +0000 http://jugad2.blogspot.com/2012/11/the-python-data-model.html Python Diary: Been playing around with Object Databases Sun, 18 Nov 2012 20:40:29 +0000 http://www.pythondiary.com/blog/Nov.18,2012/been-...-object-databases.html Vasudev Ram: Wakari, Scientific Python in the cloud Sun, 18 Nov 2012 20:19:41 +0000 http://jugad2.blogspot.com/2012/11/wakari-scientific-python-in-cloud.html Jesse Jiryu Davis: Toro: synchronization primitives for Tornado coroutines Sun, 18 Nov 2012 20:17:49 +0000 http://feedproxy.google.com/~r/EmptysquarePython/~3/_DOZT2Kd0hQ/ |
很顯然,如果你想做進一步的處理,你需要替換 print() 語句來完成其他有趣的事。
在很多應用程序中處理XML編碼格式的數據是很常見的。 不僅因為XML在Internet上面已經被廣泛應用于數據交換, 同時它也是一種存儲應用程序數據的常用格式(比如字處理,音樂庫等)。 接下來的討論會先假定讀者已經對XML基礎比較熟悉了。
在很多情況下,當使用XML來僅僅存儲數據的時候,對應的文檔結構非常緊湊并且直觀。 例如,上面例子中的RSS訂閱源類似于下面的格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<? xml version = "1.0" ?> < rss version = "2.0" xmlns:dc = "http://purl.org/dc/elements/1.1/" > < channel > < title >Planet Python</ title > < link >http://planet.python.org/</ link > < language >en</ language > < description >Planet Python - http://planet.python.org/</ description > < item > < title >Steve Holden: Python for Data Analysis</ title > < guid >http://holdenweb.blogspot.com/...-data-analysis.html</ guid > < link >http://holdenweb.blogspot.com/...-data-analysis.html</ link > < description >...</ description > < pubDate >Mon, 19 Nov 2012 02:13:51 +0000</ pubDate > </ item > < item > < title >Vasudev Ram: The Python Data model (for v2 and v3)</ title > < guid >http://jugad2.blogspot.com/...-data-model.html</ guid > < link >http://jugad2.blogspot.com/...-data-model.html</ link > < description >...</ description > < pubDate >Sun, 18 Nov 2012 22:06:47 +0000</ pubDate > </ item > < item > < title >Python Diary: Been playing around with Object Databases</ title > < guid >http://www.pythondiary.com/...-object-databases.html</ guid > < link >http://www.pythondiary.com/...-object-databases.html</ link > < description >...</ description > < pubDate >Sun, 18 Nov 2012 20:40:29 +0000</ pubDate > </ item > ... </ channel > </ rss > |
xml.etree.ElementTree.parse() 函數解析整個XML文檔并將其轉換成一個文檔對象。 然后,你就能使用 find() 、iterfind() 和 findtext() 等方法來搜索特定的XML元素了。 這些函數的參數就是某個指定的標簽名,例如 channel/item 或 title 。
每次指定某個標簽時,你需要遍歷整個文檔結構。每次搜索操作會從一個起始元素開始進行。 同樣,每次操作所指定的標簽名也是起始元素的相對路徑。 例如,執行 doc.iterfind('channel/item') 來搜索所有在 channel 元素下面的 item 元素。 doc 代表文檔的最頂層(也就是第一級的 rss 元素)。 然后接下來的調用 item.findtext() 會從已找到的 item 元素位置開始搜索。
ElementTree 模塊中的每個元素有一些重要的屬性和方法,在解析的時候非常有用。 tag 屬性包含了標簽的名字,text 屬性包含了內部的文本,而 get() 方法能獲取屬性值。例如:
1
2
3
4
5
6
7
8
9
10
11
|
>>> doc <xml.etree.ElementTree.ElementTree object at 0x101339510 > >>> e = doc.find( 'channel/title' ) >>> e <Element 'title' at 0x10135b310 > >>> e.tag 'title' >>> e.text 'Planet Python' >>> e.get( 'some_attribute' ) >>> |
有一點要強調的是 xml.etree.ElementTree 并不是XML解析的唯一方法。 對于更高級的應用程序,你需要考慮使用 lxml 。 它使用了和ElementTree同樣的編程接口,因此上面的例子同樣也適用于lxml。 你只需要將剛開始的import語句換成 from lxml.etree import parse 就行了。 lxml 完全遵循XML標準,并且速度也非??欤瑫r還支持驗證,XSLT,和XPath等特性。
增量式解析大型XML文件
任何時候只要你遇到增量式的數據處理時,第一時間就應該想到迭代器和生成器。 下面是一個很簡單的函數,只使用很少的內存就能增量式的處理一個大型XML文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
from xml.etree.ElementTree import iterparse def parse_and_remove(filename, path): path_parts = path.split( '/' ) doc = iterparse(filename, ( 'start' , 'end' )) # Skip the root element next (doc) tag_stack = [] elem_stack = [] for event, elem in doc: if event = = 'start' : tag_stack.append(elem.tag) elem_stack.append(elem) elif event = = 'end' : if tag_stack = = path_parts: yield elem elem_stack[ - 2 ].remove(elem) try : tag_stack.pop() elem_stack.pop() except IndexError: pass |
為了測試這個函數,你需要先有一個大型的XML文件。 通常你可以在政府網站或公共數據網站上找到這樣的文件。 例如,你可以下載XML格式的芝加哥城市道路坑洼數據庫。 在寫這本書的時候,下載文件已經包含超過100,000行數據,編碼格式類似于下面這樣:
假設你想寫一個腳本來按照坑洼報告數量排列郵編號碼。你可以像這樣做:
1
2
3
4
5
6
7
8
9
10
|
from xml.etree.ElementTree import parse from collections import Counter potholes_by_zip = Counter() doc = parse( 'potholes.xml' ) for pothole in doc.iterfind( 'row/row' ): potholes_by_zip[pothole.findtext( 'zip' )] + = 1 for zipcode, num in potholes_by_zip.most_common(): print (zipcode, num) |
這個腳本唯一的問題是它會先將整個XML文件加載到內存中然后解析。 在我的機器上,為了運行這個程序需要用到450MB左右的內存空間。 如果使用如下代碼,程序只需要修改一點點:
1
2
3
4
5
6
7
8
9
|
from collections import Counter potholes_by_zip = Counter() data = parse_and_remove( 'potholes.xml' , 'row/row' ) for pothole in data: potholes_by_zip[pothole.findtext( 'zip' )] + = 1 for zipcode, num in potholes_by_zip.most_common(): print (zipcode, num) |
結果是:這個版本的代碼運行時只需要7MB的內存–大大節約了內存資源。
討論
這里技術會依賴 ElementTree 模塊中的兩個核心功能。 第一,iterparse() 方法允許對XML文檔進行增量操作。 使用時,你需要提供文件名和一個包含下面一種或多種類型的事件列表: start , end, start-ns 和 end-ns 。 由 iterparse() 創建的迭代器會產生形如 (event, elem) 的元組, 其中 event 是上述事件列表中的某一個,而 elem 是相應的XML元素。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
>>> data = iterparse('potholes.xml',('start','end')) >>> next(data) ('start', < Element 'response' at 0x100771d60>) >>> next(data) ('start', < Element 'row' at 0x100771e68>) >>> next(data) ('start', < Element 'row' at 0x100771fc8>) >>> next(data) ('start', < Element 'creation_date' at 0x100771f18>) >>> next(data) ('end', < Element 'creation_date' at 0x100771f18>) >>> next(data) ('start', < Element 'status' at 0x1006a7f18>) >>> next(data) ('end', < Element 'status' at 0x1006a7f18>) >>> |
start 事件在某個元素第一次被創建并且還沒有被插入其他數據(如子元素)時被創建。 而 end 事件在某個元素已經完成時被創建。 盡管沒有在例子中演示, start-ns 和 end-ns 事件被用來處理XML文檔命名空間的聲明。
這本節例子中, start 和 end 事件被用來管理元素和標簽棧。 棧代表了文檔被解析時的層次結構, 還被用來判斷某個元素是否匹配傳給函數 parse_and_remove() 的路徑。 如果匹配,就利用 yield 語句向調用者返回這個元素。
在 yield 之后的下面這個語句才是使得程序占用極少內存的ElementTree的核心特性:
1
|
elem_stack[ - 2 ].remove(elem) |
這個語句使得之前由 yield 產生的元素從它的父節點中刪除掉。 假設已經沒有其它的地方引用這個元素了,那么這個元素就被銷毀并回收內存。
對節點的迭代式解析和刪除的最終效果就是一個在文檔上高效的增量式清掃過程。 文檔樹結構從始自終沒被完整的創建過。盡管如此,還是能通過上述簡單的方式來處理這個XML數據。
這種方案的主要缺陷就是它的運行性能了。 我自己測試的結果是,讀取整個文檔到內存中的版本的運行速度差不多是增量式處理版本的兩倍快。 但是它卻使用了超過后者60倍的內存。 因此,如果你更關心內存使用量的話,那么增量式的版本完勝。