本節書摘來自異步社群《python 3程式開發指南(第2版•修訂版)》一書中的第7章,第7.3節,作者[英]mark summerfield,王弘博,孫傳慶 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
有些程式将其處理的所有資料都使用xml檔案格式,還有些其他程式将xml用作一種便利的導入/導出格式。即便程式的主要格式是文本格式或二進制格式,導入與導出xml的能力也是有用的,并且始終是值得考慮的一項功能。
python提供了3種寫入xml檔案的方法:手動寫入xml;建立元素樹并使用其write()方法;建立dom并使用其write()方法。xml檔案的讀入與分析則有4種方法:人工讀入并分析xml(不建議采用這種方法,這裡也沒有進行講述——正确處理某些更晦澀和更進階的可能是非常困難的);使用元素樹;dom(文檔對象模型);sax(simple api for xml,用于xml的簡單api)分析器。
圖7-5給出了航空器事故記錄的xml格式。在本節中,我們就來展示如何手動寫入xml格式與如何使用元素樹、dom寫入,以及如何使用元素樹、dom、sax分析器讀入并分析xml檔案。如果你并不關心采用哪種方法讀、寫xml檔案,就可以在閱讀完“元素樹”小節,直接跳到本章的7.4節(随機存取二進制檔案)。
使用元素樹寫入xml資料分為兩個階段:首先,要建立用于表示xml資料的元素樹;之後,将元素樹寫入到檔案中。有些程式可能使用元素樹作為其資料結構,這種情況下,第一階段可以省略,隻需要直接寫入資料。我們分兩個部分來檢視export_xml_etree()方法:

我們從建立根元素()開始,之後對所有事故記錄進行疊代。對每條事故記錄,我們建立一個元素()來存放該事故記錄的資料,并使用關鍵字參數來提供屬性。所有屬性必須都是文本,是以,我們需要對日期、數值型資料、布爾型資料項進行相應轉換。我們不必擔心對“&”、“<”、“>”(或屬性值中的引号)的轉義處理,因為元素樹子產品(以及som、sax子產品)會對相關的詳細資料進行自動處理。
每個包含兩個子元素,一個用于存放機場名,另一個用于存放叙述性文本。建立子元素時,必須為其提供父元素與标簽名。元素的讀/寫text屬性則用于存放其文本。
及其所有屬性、子元素與建立之後,我們将其添加到樹體系的根()元素,反複進行這一過程,最終的元素體系中就包含了所有事故記錄資料,這些資料可以轉換為元素樹。
寫入xml資料來表示一個完整的元素樹,實際上隻是使用給定的編碼格式将元素樹本身寫入到檔案中。
到現在為止,在指定編碼格式時,我們幾乎總是使用字元串"utf8",這對python内置的open()函數而言是可以正常工作的,該函數可以接受很多種編碼方式以及這些編碼方式名稱的變種,比如“utf-8”、“utf8”、“utf-8”以及“utf8”。但對xml檔案而言,編碼方式名稱隻能是正式名稱,是以,“utf8”是不能接受的,這也是為什麼我們嚴格地使用“utf-8”。1
使用元素樹讀取xml檔案并不比寫入難多少,也分為兩個階段:首先讀入并分析xml檔案,之後對生成的元素樹進行周遊,以便讀取資料來生成incidents字典。同樣地,如果元素樹本身已經是記憶體中存儲的資料結構,第二階段就不是必要的。下面分兩部分給出import_xml_etree()方法。
預設情況下,元素樹分析器使用expat xml分析器,這也是為什麼我們必須做好捕獲expat異常的準備。
準備好元素樹之後,就可以使用xml.etree.elementtree.findall()方法對每個進行疊代處理了。每個事故都是以一個xml.etree.element對象的形式傳回的。在處理元素屬性時,我們使用的是與前面import_text_regex()方法中同樣的技術——我們首先将所有值存儲到data字典中,之後将日期、數字、布爾型值轉換到正确的類型。對機場屬性與叙述性文本元素,我們使用xml.etree.element.find()方法尋找這些值,并讀取其text屬性。如果某個文本元素不包含文本,那麼其text屬性将為none,是以,在讀取叙述性文本元素時,我們必須考慮這一點,因為該元素可以為空。在所有情況下,傳回給我們的屬性值與文本都不包含xml轉義,因為其是自動非轉義的。
與用于處理航空器事故資料的所有xml分析器類似,如果航空器或叙述性文本元素丢失,或某個屬性丢失,或某個轉換過程失敗,或任意的數值型資料超出了取值範圍,都會産生異常——這将確定無效資料被終止分析并輸出錯誤消息。用于建立并存儲事故記錄以及處理異常的代碼與前面看到的相同。
dom是一種用于表示與操縱記憶體中xml文檔的标準api。用于建立dom并将其寫入到檔案的代碼,以及使用dom對xml檔案進行分析的代碼,在結構上與元素樹代碼非常相似,隻是稍長一些。
我們首先分兩個部分檢視export_xml_dom()方法。這一方法分為兩個階段:首先建立一個dom來表示事故記錄資料,之後将該dom寫入到檔案。就像使用元素樹寫入時一樣,有些程式可能使用dom作為其資料結構,在這種情況下可以省略第一步,直接寫入資料。
該方法從擷取一個dom實作開始,預設情況下,dom實作是由expat xml分析器提供的,xml.dom.minidom子產品提供了一個比xml.dom子產品所提供的更簡單、更短小的dom實作,盡管該子產品使用的對象來自于xml.dom子產品。擷取了dom實作後,我們可以建立一個文檔。xml.dom.domimplementation.createdocument()的第一個參數是名稱空間uri——我們并不需要,是以将其指派為none;第二個參數是一個限定名(根元素的标簽名);第三個參數是文檔類型,同樣,也将其指派為none,因為我們沒有文檔類型。在擷取了表示文檔的樹之後,我們取回根元素,之後對所有事故記錄進行疊代。
對每個事故記錄,我們建立一個元素,對事故的每個屬性,我們使用該屬性名與值調用setattribute()。就像元素樹中一樣,我們也不需要擔心“&”、“<”與“>”(或屬性值中的引号)的轉義問題。對機場與叙述性文本元素,我們必須建立一個文本元素來存放文本,并以一個通常的元素(帶有适當的标簽名)作為文本元素的父親——之後,我們将該通常元素(及其包含的文本元素)添加到目前的事故元素中。事故元素完整後,就将其添加到根。
我們沒有給出except語句塊以及finally語句塊,因為這與我們前面已經看到的都是相同的。從上面的代碼中可以清晰看到的是,内置的open()函數使用的編碼字元串與用于xml檔案的編碼字元串之間的差别,這一點在前面也已讨論。
将xml文檔導入到dom中與導入到元素樹中是類似的,但與從元素樹中導出類似,導入到dom也需要更多的代碼。我們将分3個部分來檢視import_xml_dom()函數,下面先給出其def行以及嵌套的get_text()函數。
get_text()函數在一個節點清單(比如某節點的子節點)上進行疊代,對每個文本節點,提取該節點的文本并将其附加到文本清單中。最後,該函數傳回已收集到一個單獨的字元串中的所有文本,并且剝離掉兩端的空白字元。
使用dom分析xml檔案是容易的,因為子產品為我們完成了所有困難的工作,但是我們必須做好處理expat錯誤的準備,因為就像元素樹一樣,expat xml分析器也是dom類使用的預設分析器。
dom存在後,我們清空目前的事故記錄資料,并對所有事故标簽進行疊代。每次疊代時,我們都提取其屬性,對日期、數值型以及布爾型等資料,我們都将其轉換為适當的類型,就像使用元素樹時所做的一樣。使用dom與使用元素樹之間真正較大的差別是對文本節點的處理過程,我們使用xml.dom.element.getelementsbytagname()方法擷取給定标簽名的子元素——對與,我們知道總是會有其中的一個,是以我們取每個類型的第一個(唯一的一個),之後使用嵌套的get_text()函數對這些标簽的子節點進行疊代,以便提取其文本。
與通常一樣,如果有任何錯誤産生,我們就将捕獲相關的異常,為使用者列印錯誤消息,并傳回false。
dom與元素樹方法之間的差别并不大,由于兩者都使用同樣的expat分析器,是以兩者都非常快。
将預存的元素樹或dom寫成xml文檔可以使用單獨的方法調用完成。如果資料本身不是以這兩種形式存在,我們就必須先建立元素樹或dom,之後直接寫出資料會更加友善。
寫xml檔案時,我們必須確定正确地對文本與屬性值進行了轉義處理,并且寫的是格式正确的xml文檔。下面給出export_xml_manual()方法,該方法用于以xml格式寫出事故資料。
正如本章中我們通常所做的一樣,我們也忽略了except語句塊與finally語句塊。
我們使用utf-8編碼寫檔案,并且必須為内置的open()函數指定該編碼方式。嚴格地說,我們并不需要在<?xml?> 聲明中指定該編碼,因為utf-8是預設的編碼格式,但我們更願意清晰地指定。我們選擇使用雙引号(")來封裝所有屬性值,并且,為友善起見,我們使用單引号來封裝事故資料中的字元串,以避免對引号進行轉義處理的需要。
sax.saxutils.quoteattr()函數與sax.saxutils.escape()函數(我們使用這一函數處理xml文本,因為該函數可以正确地對“&”、“<”、“>”等字元進行轉義處理)類似,此外,該函數還可以對引号進行轉義(如果需要),并傳回已經使用引号包含了的字元串 ,這也是為什麼我們不需要對報告id以及其他字元串屬性值加引号的原因所在。
叙述性文本中插入的換行與文本包裹純粹是為了裝飾用的,其目的是為了使其更便于人的閱讀和編輯,但也可以忽略。
以html格式寫資料與以xml格式并沒有太大的差别。convert-incidents.py程式包含的export_html()函數是一個簡單的執行個體,這裡沒有給出該函數,因為其中沒有什麼新東西。
與元素樹和dom在記憶體中表示整個xml文檔不同的是,sax分析器是逐漸讀入并處理的,進而可能更快,對記憶體的需求也不那麼明顯。然而,性能上的優勢不能僅靠假設,尤其是元素樹與dom都使用了快速的expat分析器。
在遇到開始标簽、結束标簽以及其他xml元素時,sax分析器宣稱“分析事件”并進行工作。為處理那些我們感興趣的事件,我們必須建立一個适當的處理者類,并提供某些預定義的方法,在比對分析事件發生時,就會調用這些方法。最常實作的處理者是内容處理者,當然,如果我們需要更好的控制,提供錯誤處理者以及其他處理者也是可能的。
下面給出的是完整的import_xml_sax()方法,由于大部分工作都已經由自定義的incidentsaxhandler類實作,是以,這一方法的實作代碼很短。
我們首先建立了要使用的處理者,之後建立一個sax分析器,并将其内容處理者設定為我們剛建立的那個。之後,我們将檔案名賦予分析器的parse()方法,如果沒有分析錯誤産生,就傳回true。
我們将self(也就是說,這個incidentcollection dict子類)傳遞給自定義的incidentsaxhandler類的初始化程式。處理者清空舊的事故記錄,之後随着對檔案分析的程序建立起一個事故字典。分析完成後,該字典将包含讀入的所有事故。
自定義的sax處理者類必須繼承自适當的基類,這将確定對于任何我們沒有重新實作的方法(因為我們不關心這些方法處理的分析事件),都會調用該方法的基類版本,并且實際上不做任何處理。
我們首先調用基類的初始化程式。對所有子類而言,這通常是一種好的做法,盡管對直接的object子類而言這樣做沒有必要(但也沒有壞處)。字典self.__data用于儲存某個事故的資料,self.__text字元串用于存放機場名的文本資訊或叙述性文本的文本資訊,這依賴于我們目前正在讀入的具體内容,self.__incidents字典是到incidentcollec-tion字典(對這一字典,處理者直接對其進行更新操作)的對象引用。(一種替代的設計方案是将一個獨立的字典放置在處理者内部,并在最後使用dict.clear()将其複制到incidentcollection,之後調用dict.update()。)
在讀取到開始标簽及其屬性的任何時候,都會以标簽名以及标簽屬性作為參數來調用xml.sax.handler.content-handler.startelement()方法。對航空器事故xml檔案,開始标簽是,我們将忽略該标簽;标簽,我們使用其屬性來生成self.__data字典的一部分;标簽與标簽,兩者我們都忽略。在讀取到開始标簽時,我們總是清空self.__text字元串,因為在航空器事故xml檔案格式中,沒有嵌套的文本标簽。
在incidentsaxhandler類中,我們沒有進行任何異常處理。如果産生異常,就将傳遞給調用者,這裡也就是import_xml_sax()方法,調用者将捕獲異常,并輸出适當的錯誤消息。
讀取到結束标簽時,将調用xml.sax.handler.contenthandler.endelement()方法。如果已經到達某條事故記錄的結尾,此時應該已具備所有必要的資料,是以,此時建立一個新的incident對象,并将其添加到事故字典。如果已到達文本元素的結尾,就像self.__data 字典中添加一個項(其中包含迄今為止累積的文本)。最後,我們清空self.__text字元串,以備後面使用。(嚴格地說,我們也沒必要對其進行清空,因為在擷取開始标簽時也可以清空該字元串,但對有些xml格式,清空該字元串會有一定的作用,比如對标簽可以嵌套的情況。)
讀取到文本時,sax分析器将調用xml.sax.handler.contenthandler.characters()方法,但并不能保證對所有文本隻調用一次該方法,因為文本可能以分塊的形式出現,這也是為什麼我們隻是簡單地使用該方法來累積文本,而隻有在讀取到相關的結束标簽後才真正将文本放置到字典中。(一種更高效的實作方案是将self.__text作為一個清單,這一方法的主體部分則使用self.__text.append(text),其他方法也相應調整。)
與使用元素樹或dom相比,使用sax api是非常不同的,但确實也是很有效的。我們可以提供其他處理者,并在内容處理者中重新實作額外的方法,以便按我們的需要施加更多的控制。sax分析器本身并不儲存xml文檔的任意表示形式——這使得sax适合于将xml讀入到我們的自定義資料組合中,也意味着沒有sax“文檔”以xml格式寫出,是以,對寫xml而言,我們必須使用本章前面描述的某種方法。