1. xml.etree.ElementTree XML操縱API
ElementTree庫提供了一些工具,可以使用基於事件和基於文檔的API來解析XML,可以用XPath表達式搜索已解析的文檔,還可以創建新文檔或修改現有文檔。
1.1 解析XML文檔
已解析的XML文檔在內存中由ElementTree和Element對象表示,這些對象基於XML文檔中節點嵌套的方式按樹結構互相連接。
用parse()解析一個完整的文檔時,會返回一個ElementTree實例。這個樹了解輸入文檔中的所有數據,另外可以原地搜索或操縱樹中的節點。基於這種靈活性,可以更方便的處理已解析的文檔,不過,與基於事件的解析方法相比,這種方法往往需要更多的內存,因為必須一次加載整個文檔。
對於簡單的小文檔(如下面的播客列表,被表示為一個OPML大綱),內存需求不大。
podcasts.opml:
<?xml version="1.0" encoding="UTF-8"?> <opml version="1.0"> <head> <title>My Podcasts</title> <dateCreated>Sat, 06 Aug 2016 15:53:26 GMT</dateCreated> <dateModified>Sat, 06 Aug 2016 15:53:26 GMT</dateModified> </head> <body> <outline text="Non-tech"> <outline text="99% Invisible" type="rss" xmlUrl="http://feeds.99percentinvisible.org/99percentinvisible" htmlUrl="http://99percentinvisible.org" /> </outline> <outline text="Python"> <outline text="Talk Python to Me" type="rss" xmlUrl="https://talkpython.fm/episodes/rss" htmlUrl="https://talkpython.fm" /> <outline text="Podcast.__init__" type="rss" xmlUrl="http://podcastinit.podbean.com/feed/" htmlUrl="http://podcastinit.com" /> </outline> </body> </opml>
要解析這個文檔,需要向parse()傳遞一個打開的文件句柄。
from xml.etree import ElementTree with open('podcasts.opml', 'rt') as f: tree = ElementTree.parse(f) print(tree)
這個方法會讀取數據、解析XML,並返回一個ElementTree對象。
1.2 遍歷解析樹
要按順序訪問所有子節點,可以使用iter()創建一個生成器,該生成器迭代處理這個ElementTree實例。
from xml.etree import ElementTree with open('podcasts.opml', 'rt') as f: tree = ElementTree.parse(f) for node in tree.iter(): print(node.tag)
這個例子會打印整個樹,一次打印一個標記。
如果只是打印播客的名字組和提要URL,則可以只迭代處理outline節點(而不考慮首部中的所有數據),並且通過查找attrib字典中的值來打印text和xmlUrl屬性。
from xml.etree import ElementTree with open('podcasts.opml', 'rt') as f: tree = ElementTree.parse(f) for node in tree.iter('outline'): name = node.attrib.get('text') url = node.attrib.get('xmlUrl') if name and url: print(' %s' % name) print(' %s' % url) else: print(name)
iter()的'outline'參數意味着只處理標記為'outline'的節點。
1.3 查找文檔中的節點
查看整個樹並搜索有關的節點可能很容易出錯。前面的例子必須查看每一個outline節點,來確定這是一個組(只有一個text屬性的節點)還是一個播客(包含text和xmlUrl的節點)。要生成一個簡單的播客提要URL列表而不包含名字或組,可以簡化邏輯,使用findall()來查找有更多描述性搜索特性的節點。
對以上第一個版本做出第一次修改,用一個XPath參數來查找所有outline節點。
from xml.etree import ElementTree with open('podcasts.opml', 'rt') as f: tree = ElementTree.parse(f) for node in tree.findall('.//outline'): url = node.attrib.get('xmlUrl') if url: print(url)
這個版本中的邏輯與使用getiterator()的版本並沒有顯著區別。這里仍然必須檢查是否存在URL,只不過如果沒有發現URL,它不會打印組名。
outline節點只有兩層嵌套,可以利用這一點,把搜索路徑修改為.//outline/outline,這意味着循環只處理outline節點的第二層。
from xml.etree import ElementTree with open('podcasts.opml', 'rt') as f: tree = ElementTree.parse(f) for node in tree.findall('.//outline/outline'): url = node.attrib.get('xmlUrl') print(url)
輸入中所有嵌套深度為兩層的outline節點都認為有一個xmlURL屬性指向播客提要,所以循環在使用這個屬性之前可以不做檢查。
不過,這個版本僅限於當前的這個結構,所以如果outline節點重新組織為一個更深的樹,那么這個版本就無法正常工作了。
1.4 解析節點屬性
findall()和iter()返回的元素是Element對象,各個對象分別表示XML解析樹中的一個節點。每個Element都有一些屬性可以用來獲取XML中的數據。可以用一個稍有些牽強的示例輸入文件data.xml來說明這種行為。
<?xml version="1.0" encoding="UTF-8"?> <top> <child>Regular text.</child> <child_with_tail>Regular text.</child_with_tail>"Tail" text. <with_attributes name="value" foo="bar"/> <entity_expansion attribute="This & That"> That & This </entity_expansion> </top>
可以由attrib屬性得到節點的XML屬性,attrib屬性就像是一個字典。
from xml.etree import ElementTree with open('data.xml', 'rt') as f: tree = ElementTree.parse(f) node = tree.find('./with_attributes') print(node.tag) for name,value in sorted(node.attrib.items()): print(name,value)
輸入文件第5行上的節點有兩個屬性name和foo。
還可以得到節點的文本內容,以及結束標記后面的tail文本。
from xml.etree import ElementTree with open('data.xml', 'rt') as f: tree = ElementTree.parse(f) for path in ['./child','./child_with_tail']: node = tree.find(path) print(node.tag) print('child node text:',node.text) print('and tail text:',node.tail)
第3行上的child節點包含嵌入文本,第4行的節點包含帶tail的文本(包括空白符)。
返回值之前,文檔中嵌入的XML實體引用會被轉換為適當的字符。
from xml.etree import ElementTree with open('data.xml', 'rt') as f: tree = ElementTree.parse(f) node = tree.find('entity_expansion') print(node.tag) print('in attribute:',node.attrib['attribute']) print('in text:',node.text.strip())
這個自動轉換意味着可以忽略XML文檔中表示某些字符的實現細節。
1.5 解析時監視事件
另一個處理XML文檔的API是基於事件的。解析器為開始標記生成start事件,為結束標記生成end事件。解析階段中可以通過迭代處理事件流從文檔抽取數據,如果以后沒有必要處理整個文檔,或者沒有必要將解析文檔都保存在內存中,那么基於事件的API就會很方便。
有以下事件類型:
start遇到一個新標記。會處理標記的結束尖括號,但不處理內容。
end已經處理結束標記的結束尖括號。所有子節點都已經處理。
start-ns結束一個命名空間聲明。
end-ns結束一個命名空間聲明。
iterparse()返回一個iterable,它會生成元組,其中包含事件名和觸發事件的節點。
from xml.etree.ElementTree import iterparse depth = 0 prefix_width = 8 prefix_dots = '.' * prefix_width line_template = '.'.join([ '{prefix:<0.{prefix_len}}', '{event:<8}', '{suffix:<{suffix_len}}', '{node.tag:<12}', '{node_id}', ]) EVENT_NAMES = ['start','end','start-ns','end-ns'] for (event,node) in iterparse('podcasts.opml',EVENT_NAMES): if event == 'end': depth -= 1 prefix_len = depth * 2 print(line_template.format( prefix = prefix_dots, prefix_len = prefix_len, suffix = '', suffix_len = (prefix_width - prefix_len), node = node, node_id = id(node), event = event, )) if event == 'start': depth += 1
默認的,只會生成end事件。要查看其他事件,可以將所需的事件名列表傳入iterparse()。
以事件方式進行處理對於某些操作來說更為自然,如將XML輸入轉換為另外某種格式。可以使用這個技術將播可列表(來自前面的例子)從XML文件轉換為一個CSV文件,以便把它們加載到一個電子表格或數據庫應用。
import csv import sys from xml.etree.ElementTree import iterparse writer = csv.writer(sys.stdout,quoting=csv.QUOTE_NONNUMERIC) group_name = '' parsing = iterparse('podcasts.opml',events=['start']) for (event,node) in parsing: if node.tag != 'outline': # Ignore anything not part of the outline. continue if not node.attrib.get('xmlUrl'): #Remember the current group. group_name = node.attrib['text'] else: #Output a podcast entry. writer.writerow( (group_name,node.attrib['text'], node.attrib['xmlUrl'], node.attrib.get('htmlUrl','')) )
這個轉換程序並不需要將整個已解析的輸入文件保存在內存中,其在遇到輸入中的各個節點時才進行處理,這樣做會更為高效。
1.6 創建一個定制樹構造器
要處理解析事件,一種可能更高效的方法是將標准的樹構造器行為替換為一種定制行為。XMLParser解析器使用一個TreeBuilder處理XML,並調用目標類的方法保存結果。通常輸出是由默認TreeBuilder類創建的一個ElementTree實例。可以將TreeBuilder替換為另一個類,使它在實例化Element節點之前接收事件,從而節省這部分開銷。
可以將XML-CSV轉換器重新實現為一個樹構造器。
import io import csv import sys from xml.etree.ElementTree import XMLParser class PodcastListToCSV(object): def __init__(self,outputFile): self.writer = csv.writer( outputFile, quoting = csv.QUOTE_NONNUMERIC, ) self.group_name = '' def start(self,tag,attrib): if tag != 'outline': # Ignore anything not part of the outline. return if not attrib.get('xmlUrl'): #Remember the current group. self.group_name = attrib['text'] else: #Output a pddcast entry. self.writer.writerow( (self.group_name, attrib['text'], attrib['xmlUrl'], attrib.get('htmlUrl','')) ) def end(self,tag): "Ignore closing tags" def data(self,data): "Ignore data inside nodes" def close(self): "Nothing special to do here" target = PodcastListToCSV(sys.stdout) parser = XMLParser(target=target) with open('podcasts.opml','rt') as f: for line in f: parser.feed(line) parser.close()
PodcastListToCSV實現了TreeBuilder協議。每次遇到一個新的XML標記時,都會調用start()並提供標記名和屬性。看到一個結束標記時,會根據這個標記名調用end()。在這二者之間,如果一個節點有內容,則會調用data()(一般認為樹構造器會跟蹤“當前”節點)。在所有輸入都已經被處理時,將調用close()。它會返回一個值,返回給XMLTreeBuilder的用戶。
1.7 用元素節點構造文檔
除了解析功能,xml.etree.ElementTree還支持由應用中構造的Element對象來創建良構的XML文檔。解析文檔時使用的Element類還知道如何生成其內容的一個串行化形式,然后可以將這個串行化內容寫至一個文件或其他數據流。
有3個輔助函數對於創建Element節點層次結構很有用。Element()創建一個標准節點,SubElement()將一個新節點關聯到一個父節點,Comment()創建一個使用XML注釋語法串行化數據的節點。
from xml.etree.ElementTree import Element,SubElement,Comment,tostring top = Element('top') comment = Comment('Generated for PyMOTW') top.append(comment) child = SubElement(top,'child') child.text = 'This child contains text.' child_with_tail = SubElement(top,'child_with_tail') child_with_tail.text = 'This child has text.' child_with_tail.tail = 'And "tail" text.' child_with_entity_ref = SubElement(top,'child_with_entity_ref') child_with_entity_ref.text = 'This & that' print(tostring(top))
這個輸出只包含樹中的XML節點,而不包含版本和編碼的XML聲明。
1.8 美觀打印XML
ElementTree不會通過格式化tostring()的輸出來提高可讀性,因為增加額外的空白符會改變文檔的內容。為了讓輸出更易讀,后面的例子將使用xml.dom.minidom解析XML,然后使用它的toprettyxml()方法。
from xml.etree import ElementTree from xml.dom import minidom from xml.etree.ElementTree import Element,SubElement,Comment,tostring def prettify(elem): """ Return a pretty-printed XML string for the Element. """ rough_string = ElementTree.tostring(elem,'utf-8') reparsed = minidom.parseString(rough_string) return reparsed.toprettyxml(indent=" ") top = Element('top') comment = Comment('Generated for PyMOTW') top.append(comment) child = SubElement(top,'child') child.text = 'This child contains text.' child_with_tail = SubElement(top,'child_with_tail') child_with_tail.text = 'This child has text.' child_with_tail.tail = 'And "tail" text.' child_with_entity_ref = SubElement(top,'child_with_entity_ref') child_with_entity_ref.text = 'This & that' print(prettify(top))
輸出變得更易讀。
除了增加用於格式化的額外空白符,xml.dom.minidom美觀打印器還會向輸出增加一個XML聲明。