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宣告,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163628.html
標籤:Python
下一篇:django時區問題
