Python:minidom模組 用於解

一、XML解析 下面我講述一下,如何使用minidom來讀取和保存XML文件。

下面是片段分類的一個示例文–catalog.xml

<?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <item id="1"> <caption>Python</caption> <item id="4"> <caption>測試</caption> </item> </item> <item id="2"> <caption>Zope</caption> </item> </catalog>

分類是樹狀結構,顯示出來可能為:

Python 測試 Zope

先簡單介紹一下XML的知識,如果你已經知道了可以跳過去。

1. XML文檔的編碼

此XML文檔的編碼為utf-8,因此你看到的「測試」其實是UTF-8編碼。在XML文檔的處理中都是使用UTF-8編碼進行的,因此,如果你不寫明encoding的話,都是認為文件是UTF-8編碼的。在Python中,好象只支援幾種編碼,象我們常用的GB2312碼就不支援,因此建議大家在處理XML時使用UTF-8編碼。

2. XML文檔的結構

XML文檔有XML頭資訊和XML資訊體。頭資訊如:

<?xml version="1.0" encoding="utf-8"?>

它表明了此XML文檔所用的版本,編碼方式。有些複雜的還有一些文檔類型的定義(DOCTYPE),用於定義此XML文檔所用的DTD或Schema和一些實體的定義。這裡並沒有用到,而且我也不是專家,就不再細說了。

XML資訊體是由樹狀元素組成。每個XML文檔都有一個文檔元素,也就是樹的根元素,所有其它的元素和內容都包含在根元素中。

3. DOM

DOM是Document Object Model的簡稱,它是以對象樹來表示一個XML文檔的方法,使用它的好處就是你可以非常靈活的在對象中進行遍歷。

4. 元素和結點

元素就是標記,它是成對出現的。XML文檔就是由元素組成的,但元素與元素之間可以有文本,元素的內容也是文本。在minidom中有許多的結點,元素也屬於結點的一種,它不是葉子結點,即它存在子結點;還存在一些葉子結點,如文本結點,它下面不再有子結點。

象catalog.xml中,文檔元素是catalog,它下面有兩種元素:maxid和item。maxid用來表示當前最大的item的id 值。每一個item都有一個id屬性,id屬性是唯一的,在 NewEdit 中用來生成每個分類所對應的程式碼片段的XML文檔名,因此不能重複,而且它是一個遞增的值。item元素有一個caption子元素,用來表示此分類項的名稱,它還可以包含item元素。這樣,就定義了一個樹狀XML結構,下面讓我們看一看如果把它們讀出來。

一、得到dom對象

>>> import xml.dom.minidom >>> dom = xml.dom.minidom.parse('d:/catalog.xml')

這樣我們得到了一個dom對象,它的第一個元素應該是catalog。

二、得到文檔元素對象

>>> root = dom.documentElement

這樣我們得到了根元素(catalog)。

三、結點屬性

每一個結點都有它的nodeName,nodeValue,nodeType屬性。nodeName為結點名字。

>>> root.nodeName u'catalog'

nodeValue是結點的值,只對文本結點有效。nodeType是結點的類型,現在有以下幾種:

'ATTRIBUTE_NODE' 'CDATA_SECTION_NODE' 'COMMENT_NODE' 'DOCUMENT_FRAGMENT_NODE' 'DOCUMENT_NODE' 'DOCUMENT_TYPE_NODE' 'ELEMENT_NODE' 'ENTITY_NODE' 'ENTITY_REFERENCE_NODE' 'NOTATION_NODE' 'PROCESSING_INSTRUCTION_NODE' 'TEXT_NODE'

這些結點通過名字很好理解。catalog是ELEMENT_NODE類型。

>>> root.nodeType 1 >>> root.ELEMENT_NODE 1

四、子元素、子結點的訪問

訪問子元素、子結點的方法很多,對於知道元素名字的子元素,可以使用getElementsByTagName方法,如讀取maxid子元素:

>>> root.getElementsByTagName_r('maxid') [<DOM Element: maxid at 0xb6d0a8>]

這樣返回一個列表,由於我們的例子中maxid只有一項,因此列表也只有一項。

如果想得到某個元素下的所有子結點(包括元素),可以使用childNodes屬性:

>>> root.childNodes [<DOM Text node "n ">, <DOM Element: maxid at 0xb6d0a8>, <DOM Text node "n ">, <DOM Element: item at 0xb6d918>, <DOM Text node "n ">, <DOM Element: item at 0xb6de40>, <DOM Text node "n ">, <DOM Element: item at 0xb6dfa8>, <DOM Text node "n">]

可以看出所有兩個標記間的內容都被視為文本結點。象每行後面的回車,都被看到文本結點。從上面的結果我們可以看出每個結點的類型,本例中有文本結點和元素結點;結點的名字(元素結點);結點的值(文本結點)。每個結點都是一個對象,不同的結點對象有不同的屬性和方法,更詳細的要參見文檔。由於本例比較簡單,只涉及文本結點和元素結點。

getElementsByTagName可以搜索當前元素的所有子元素,包括所有層次的子元素。childNodes只保存了當前元素的第一層子結點。

這樣我們可以遍歷childNodes來訪問每一個結點,判斷它的nodeType來得到不同的內容。如,列印出所有元素的名字:

>>> for node in root.childNodes: if node.nodeType == node.ELEMENT_NODE: print node.nodeName maxid item item

對於文本結點,想得到它的文本內容可以使用: .data屬性。

對於簡單的元素,如:<caption>Python</caption>,我們可以編寫這樣一個函數來得到它的內容(這裡為Python)。

def getTagText(root, tag): node = root.getElementsByTagName_r(tag)[0] rc = "" for node in node.childNodes: if node.nodeType in ( node.TEXT_NODE, node.CDATA_SECTION_NODE): rc = rc + node.data return rc

這個函數只處理找到的第一個符合的子元素。它會將符合的第一個子元素中的所有文本結點拼在一起。當nodeType為文本類結點時,node.data為文本的內容。如果我們考查一下元素caption,我們可能看到:

[<DOM Text node "Python">]

說明caption元素只有一個文本結點。

如果一個元素有屬性,那麼可以使用getAttribute方法,如:

>>> itemlist = root.getElementsByTagName_r('item') >>> item = itemlist[0] >>> item.getAttribute('id') u'1'

這樣就得到了第一個item元素的屬性值。

下面讓我們簡單地小結一下如何使用minidom來讀取XML中的資訊

1. 導入xml.dom.minidom模組,生成dom對象 2. 得到文檔對象(根對象) 3. 通過getElementsByTagName_r()方法和childNodes屬性(還有其它一些方法和屬性)找到要處理的元素 4. 取得元素下文本結點的內容

二.寫入.

下面我來演示一下如何從無到有生成象catalog.xml一樣的XML文件。

一、生成dom對象

>>> import xml.dom.minidom >>> impl = xml.dom.minidom.getDOMImplementation() >>> dom = impl.createDocument(None, 'catalog', None)

這樣就生成了一個空的dom對象。其中catalog為文檔元素名,即根元素名。

二、顯示生成的XML內容

每一個dom結點對象(包括dom對象本身)都有輸出XML內容的方法,如:toxml(), toprettyxml()

toxml()輸出緊湊格式的XML文本,如:

<catalog><item>test</item><item>test</item></catalog>

toprettyxml()輸出美化後的XML文本,如:

<catalog> <item> test </item> <item> test </item> </catalog>

可以看出,它是將每個結點後面都加入了回車符,並且自動處理縮近。但對於每一個元素,如果元素只有文本內容,則我希望元素的tag與文本是在一起的,如:

<item>test</item>

而不想是分開的格式,但minidom本身是不支援這樣的處理。關於如何實現形如:

<catalog> <item>test</item> <item>test</item> </catalog>

這樣的XML格式,後面我們再說。

三、生成各種結點對象

dom對象擁有各種生成結點的方法,下面列出文本結點,CDATA結點和元素結點的生成過程。

1. 文本結點的生成

>>> text=dom.createTextNode('test') test

要注意的是,在生成結點時,minidom並不對文本字元進行檢查,象文本中如果出現了'<','&'之類的字元,應該轉換為相應的實體符號'<','&'才可以,這裡沒有做這個處理。

2. CDATA結點的生成

>>> data = dom.createCDATASection('aaaaaanbbbbbb') >>> data.toxml() '<![CDATA[aaaaaanbbbbbb]]>'

CDATA是用於包括大塊文本,同時可以不用轉換'<','&'字元的標記,它是用<![CDATA[文本]]>來包括的。但文本中不可以有"]]>"這樣的串存在。生成結點時minidom不作這些檢查,只有當你輸出時才有可能發現有錯。

3. 元素結點的生成

>>> item = dom.createElement_x_x_x_x_x('caption') >>> item.toxml() '<caption/>'

對於象元素這樣的結點,生成的元素結點其實是一個空元素,即不包含任何文本,如果要包含文本或其它的元素,我們需要使用a() 或insertBefore()之類的方法將子結點加就到元素結點中。如將上面生成的text結點加入到caption元素結點中:

>>> item.a(text) <DOM Text node "test"> >>> item.toxml() '<caption>test</caption>'

使用元素對象的setAttribute()方法可以向元素中加入屬性,如:

>>> item.setAttribute('id', 'idvalue') >>> item.toxml() '<caption id="idvalue">test</caption>'

四、生成dom對象樹

我們有了dom對象,又知道了如何生成各種結點,包括葉子結點(不包含其它結點的結點,如文本結點)和非葉子結點(包含其它結點的結點,如元素結點)的生成,然後就需要利用結點對象本身的a()或insertBefore()方法將各個結點根據在樹中的位置連起來,串成一棵樹。最後要串到文檔結點上,即根結點上。如一個完整的示例為:

>>> import xml.dom.minidom >>> impl = xml.dom.minidom.getDOMImplementation() >>> dom = impl.createDocument(None, 'catalog', None) >>> root = dom.documentElement >>> item = dom.createElement_x_x_x_x_x('item') >>> text = dom.createTextNode('test') >>> item.a(text) <DOM Text node "test"> >>> root.a(item) <DOM Element: item at 0xb9cf80> >>> print root.toxml() <catalog><item>test</item></catalog>

五、簡單生成元素結點的函數

下面是我寫的一個小函數,用於簡單的生成類似於:

<caption>test</caption>

或形如:

<item><![CDATA[test]]></item>

的元素結點

1 def makeEasyTag(dom, tagname, value, type='text'): 2 tag = dom.createElement_x_x_x_x_x(tagname) 3 if value.find(']]>') > -1: 4 type = 'text' 5 if type == 'text': 6 value = value.replace('&', '&') 7 value = value.replace('<', '<') 8 text = dom.createTextNode(value) 9 elif type == 'cdata': 10 text = dom.createCDATASection(value) 11 tag.a(text) 12 return tag

參數說明:

  • dom為dom對象
  • tagname為要生成元素的名字,如'item'
  • value為其文本內容,可以為多行
  • type為文本結點的格式,'text'為一般Text結點,'cdata'為CDATA結點

函數處理說明:

  • 首先創建元素結點
  • 查找文本內容是否有']]>',如果找到,則此文本結點只可以是Text結點
  • 如果結點類型為'text',則對文本內容中的'<'替換為'<','&'替換為'&',再生成文本結點
  • 如果結點類型為'cdata',則生成CDATA結點
  • 將生成的文本結點追加到元素結點上

因此這個小函數可以自動地處理字元轉化及避免CDATA結點中出現']]>'串。

上面生成'item'結點的語句可以改為:

>>> item = makeEasyTag(dom, 'item', 'test') >>> item.toxml() '<item>test</item>'

六、寫入到XML文件中

dom對象樹已經生成好了,我們可以調用dom的writexml()方法來將內容寫入文件中。writexml()方法語法格式為:

writexml(writer, indent, addindent, newl, encoding)

  • writer是文件對象
  • indent是每個tag前填充的字元,如:' ',則表示每個tag前有兩個空格
  • addindent是每個子結點的縮近字元
  • newl是每個tag後填充的字元,如:'n',則表示每個tag後面有一個回車
  • encoding是生成的XML資訊頭中的encoding屬性值,在輸出時minidom並不真正進行編碼的處理,如果你保存的文本內容中有漢字,則需要自已進行編碼轉換。

writexml方法是除了writer參數必須要有外,其餘可以省略。下面給出一個文本內容有漢字的示例:

1 >>> import xml.dom.minidom 2 >>> impl = xml.dom.minidom.getDOMImplementation() 3 >>> dom = impl.createDocument(None, 'catalog', None) 4 >>> root = dom.documentElement 5 >>> text = unicode('漢字示例', 'cp936') 6 >>> item = makeEasyTag(dom, 'item', text) 7 >>> root.a(item) 8 <DOM Element: item at 0xb9ceb8> 9 >>> root.toxml() 10 u'<catalog><item>u6c49u5b57u793au4f8b</item></catalog>' 11 >>> f=file('d:/test.xml', 'w') 12 >>> import codecs 13 >>> writer = codecs.lookup('utf-8')[3](f) 14 >>> dom.writexml(writer, encoding='utf-8') 15 >>> writer.close()

5行因為XML處理時內部使用Unicode編碼,因此象漢字首先要轉成Unicode,如果你不做這一步minicode並不檢查,並且保存時可能不會出錯。但讀取時可能會出錯。 12-13行 生成UTF-8編碼的寫入流對象,這樣在保存時會自動將Unicode轉換成UTF-8編碼。

這樣寫XML文件就完成了。 三.美化.

對於dom對象的writexml()方法,雖然可以控制一些格式上的輸出,但結果並不讓人滿意。比如我想實現:

<catalog> <item>test</item> <item>test</item> </catalog>

而不是:

<catalog> <item> test </item> <item> test </item> </catalog>

如果是象下面的輸出結果我無法區分原來文本中是否帶有空白,而上一種結果則不存在這一問題。好在我在wxPython自帶的XML資源編輯器(xred)發現了美化的程式碼。程式碼如下:

1 def Indent(dom, node, indent = 0): 2 # Copy child list because it will change soon 3 children = node.childNodes[:] 4 # Main node doesn't need to be indented 5 if indent: 6 text = dom.createTextNode('n' + 't' * indent) 7 node.parentNode.insertBefore(text, node) 8 if children: 9 # Append newline after last child, except for text nodes 10 if children[-1].nodeType == node.ELEMENT_NODE: 11 text = dom.createTextNode('n' + 't' * indent) 12 node.a(text) 13 # Indent children which are elements 14 for n in children: 15 if n.nodeType == node.ELEMENT_NODE: 16 Indent(dom, n, indent + 1)

參數說明:

dom為dom對象 node為要處理的元素結點 indent指明縮近的層數

函數說明:

Indent是一個遞歸函數,當一個結點有子元素時進行遞歸處理。主要是解決子元素的換行和縮近的處理。這裡縮近是寫死的,每一級縮近使用一個製表符。如果你願意可以改為你想要的內容。就是把函數中的't'換替一下。或乾脆寫成一個全局變數,或參數以後改起來可能要容易的多。不過在 NewEdit 中,這樣的處理足夠了,就沒有做這些工作。

Indent基本的想法就是遞歸遍歷所有子結點,在所有需要加入回車和縮近的地方插入相應的文本結點。這樣再使用writexml()輸出時就是縮近好了的。具體程式不再細說,直接用就行了。

但這裡要注意的是:

Indent()要修改原dom對象,因此在調用它之前最好先複製一個臨時dom對象,使用完畢後再清除這個臨時dom對象即可。下面是詳細的調用過程:

1 domcopy = dom.cloneNode(True) 2 Indent(domcopy, domcopy.documentElement) 3 f = file(xmlfile, 'wb') 4 writer = codecs.lookup('utf-8')[3](f) 5 domcopy.writexml(writer, encoding = 'utf-8') 6 domcopy.unlink()

1行 克隆一個dom對象 2行 進行縮近處理 3-4行 進行UTF-8編碼處理 5行 生成XML文件 6行 清除dom對象的內容

經過這番處理之後,你的XML文檔應該好看多了。

去除多餘的行:

from xml.dom inport minidom def fixed_writexml(self, writer, indent="", addindent="", newl=""): # indent = current indentation # addindent = indentation to add to higher levels # newl = newline string writer.write(indent+"<" + self.tagName) attrs = self._get_attributes() a_names = attrs.keys() a_names.sort() for a_name in a_names: writer.write(" %s="" % a_name) minidom._write_data(writer, attrs[a_name].value) writer.write(""") if self.childNodes: if len(self.childNodes) == 1 and self.childNodes[0].nodeType == minidom.Node.TEXT_NODE: writer.write(">") self.childNodes[0].writexml(writer, "", "", "") writer.write("</%s>%s" % (self.tagName, newl)) return writer.write(">%s"%(newl)) for node in self.childNodes: if node.nodeType is not minidom.Node.TEXT_NODE: node.writexml(writer,indent+addindent,addindent,newl) writer.write("%s</%s>%s" % (indent,self.tagName,newl)) else: writer.write("/>%s"%(newl)) # replace minidom's function with ours minidom.Element.writexml = fixed_writexml