Python XML解析之DOM

Leohahah發表於2018-09-29
DOM說明:
DOM:Document Object Model API
DOM是一種跨語言的XML解析機制,DOM把整個XML檔案或字串在記憶體中解析為樹型結構方便訪問。
xml.dom.minidom就是DOM在Python中實現,本文主要結合minidom解釋DOM架構。

API匯入:

from xml.dom.minidom import parse
from xml.dom.minidom import parseString
import xml.dom.minidom
dom和etree是xml package目錄下的兩個subpackage,minidom和ElementTree是dom和etree下的兩個module檔案,以.py字尾,其中定義了一系列的類和方法。
Document.documentElement相當於Etree中的tree.getroot()用於獲取整個樹唯一的根節點

概念解析:

xml.dom中包含以下類:
1.DOMImplementation
2.Node 
Node是最重要的類,XML被解析為一個樹,所有的節點都是都是node的子類,這些節點可以是element、comments等等,官網列出的節點型別就有:
ELEMENT_NODE, ATTRIBUTE_NODE, TEXT_NODE, CDATA_SECTION_NODE, ENTITY_NODE, PROCESSING_INSTRUCTION_NODE, COMMENT_NODE,DOCUMENT_NODE, DOCUMENT_TYPE_NODE, NOTATION_NODE,每個節點有一個數字表示,只要是node型別就可以使用node.nodeType來判斷他屬於哪一種node,例如if child.nodeType==child.ELEMENT_NODE:或者if child.nodeType==1:  --兩者等價
3.NodeList --通過getElementsByTagName()方法返回的nodelist,此方法只有element和document兩個類有。
4.DocumentType
5.Document --整個XML檔案解析樹,包含所有element、attribute、comments、text等等,也是node的子類。
6.Element 
7.Attr --element的屬性,這個型別的node只能由Element.getAttributeNode(attrname)來獲取,無法遍歷獲取,而且其既不是element node的子節點也不是兄弟節點。幾乎從無必要獲取此節點,直接使用element類的getAttribute(attrname)來得到屬性的值即可。
8.Comment --comment節點,表示XML檔案註釋節點
9.Text  --xml.etree.ElementTree中的text表示的是element中的內容,而這裡的text型別表示一個node,這個node可以是element中的data節點也可以是element之間的換行和製表符(
	),如果是element的data內容那麼此text是element的唯一子節點,通過childNodes[0].data或firstChild.data獲取element內容,如果是換行製表符那麼此節點element的兄弟節點。
10.ProcessingInstruction
除node類之外,對於XML解析最重要的就是Document類和Element、text、Comment、Attr等類,前者配合parse()或parseString()將xml檔案或字串在記憶體中例項化為一個tree(document型別),後邊的類用於對XML樹做各種操作和查詢。

鑑於幾乎所有的可操作物件類都是繼承於node類,這裡貼一下node的各種屬性和方法的連結:

另外再列出node一些常見的屬性和方法:
Node.nodeType  --詳見上邊對Node類的解釋
Node.attributes  --只有element型別的node才有此屬性
Node.childNodes 
--返回節點的子節點nodelist,與通過getElementsByTagName()獲取nodelist的區別在於此方法只返回直接子節點而非全部子節點,此外這兩個方法的最大區別是:childNodes返回的是所有子節點的集合,而getElementsByTagName(tagName)必須指定tagName。
Node.previousSibling --node的左兄弟節點,如果沒有則返回none
Node.nextSibling --node的右兄弟節點,如果沒有則返回none
Node.nodeName --不常用,因為繼承於node的各種類都有自己的更便於識別的name屬性,例如element.tagName
Node.appendChild(newChild)
另:如果要熟練的使用minidom API,那麼請務必將https://docs.python.org/2/library/xml.dom.html 熟讀,以上列出的各種繼承於node的類都有一些自己獨特的屬性和方法,除了熟悉node類之外,熟悉這些繼承子類的方法也是很有必要的。

XML檔案解析示例:

--有一個如下的XML檔案:proxool.xml:
<?xml version="1.0" encoding="utf-8"?>
<something-else-entirely>
    <proxool>
        <alias>myPool</alias>
            <!-- mysql 連線配置,注意修改database_hostname為相應的資料庫主機名、或IP地址 --> 
         <driver-url>
            jdbc:mysql://dbsrv:3306/TEST?useUnicode=true&characterEncoding=UTF8
        </driver-url> 
        <driver-class>com.mysql.jdbc.Driver</driver-class> 
        <!-- 使用者名稱、密碼 -->
        <driver-properties>
            <property name="user" value="leo" />
            <property name="password" value="leo" />
        </driver-properties>
        <!--自動偵察各個連線狀態的時間間隔(毫秒),偵察到空閒的連線就馬上回收,超時的銷燬 -->
        <house-keeping-sleep-time>30000</house-keeping-sleep-time>
        <house-keeping-test-sql>select CURRENT_DATE from dual
        </house-keeping-test-sql>
        <!--最大連線數(預設5個),超過了這個連線數,再有請求時,就排在佇列中等候,最大的等待請求數由maximum-new-connections決定 -->
        <maximum-connection-count>120</maximum-connection-count>
        <!--最小連線數(預設2個) -->
        <minimum-connection-count>5</minimum-connection-count>
        <!--沒有空閒連線可以分配而在佇列中等候的最大請求數,超過這個請求數的使用者連線就不會被接受,該引數已經不建議使用,由simultaneous-build-throttle替代 -->
        <!--一個活動連線最大活動時間預設5分鐘 -->
        <maximum-active-time>3600000</maximum-active-time>
        <!--最少保持的空閒連線數(預設2個),如果當前的連線池中的可用連線少於這個數值, 新的連線將被建立 -->
        <prototype-count>5</prototype-count>
        <!--可一次建立的最大連線數 -->
        <simultaneous-build-throttle>20</simultaneous-build-throttle>
        <!--如果為true,那麼每個被執行的SQL語句將會在執行期被log記錄 -->
        <trace>false</trace>
    </proxool>
</something-else-entirely>

現在將其中的內容解析為如下格式:

*****
描述:最大連線數(預設5個),超過了這個連線數,再有請求時,就排在佇列中等候,最大的等待請求數由maximum-new-connections決定
配置項:maximum-connection-count
配置值:120
*****
描述:xxx
配置項:xxx
配置值:xxx
*****
......

程式碼如下:

# -*- coding:utf-8 -*-
# 本指令碼適用於Python2和3
from xml.dom.minidom import parse
import xml.dom.minidom
import sys
# file = sys.argv[1]
file = "/root/proxool.xml"
# 先寫一個判斷節點是否包含element型別子節點的判斷函式
def has_element_child(nodename):
    has_element_child = 0
    for child in nodename.childNodes:
        if child.nodeType==1:
            has_element_child += 1
    return has_element_child
# 定義解析示例XML檔案的方法
def parse_xml(file):
    if not file:
        sys.exit(0)
    tree = parse(file) # document型別的解析樹
    root = tree.getElementsByTagName(`proxool`)[0] # 將父節點定位到proxool element
    for child in root.childNodes:
        if child.nodeType==child.ELEMENT_NODE and has_element_child(child)==0: # 當node為element型別,且無element型別的子節點時
            print u`配置項`+": %s" % child.tagName
            print u`配置值`+": %s" % child.firstChild.data.strip()
        elif child.nodeType==child.ELEMENT_NODE and has_element_child(child)>0: # 當節點包含element型別子節點時
            for child_child in child.childNodes:
                if child_child.nodeType==child.ELEMENT_NODE:
                    print u`配置項`+": %s" % child_child.getAttribute(`name`)
                    print u`配置值`+": %s" % child_child.getAttribute(`value`)
        elif child.nodeType==child.COMMENT_NODE: # 當node為comment型別時
            print "*****"
            print u`描述`+": %s" % child.data
        else:
            pass
# 處理示例XML檔案
parse_xml(file)

XML檔案比較修改示例:

minidom相比於DOM API最大的差別就是新增了node.writexml()、node.toprettyxml()等方法,這兩個方法可以將你對XML解析樹作出的修改寫入檔案中,現在我們將proxool.xml copy到proxool.xml.new中,並在proxool節點下新增一個子節點<new_tag name=”Leo”>For_Test</new_tag>,我們要比較新XML檔案中比舊XML檔案新增的配置項,對舊XML的配置項不做修改,程式碼如下:
# -*- coding:utf-8 -*-
# 本指令碼適用於Python2和3
from xml.dom.minidom import parse
import xml.dom.minidom
import sys
reload(sys)
sys.setdefaultencoding("utf-8")
old_file = sys.argv[1]
new_file = sys.argv[2]
# 先寫一個判斷節點是否包含element型別子節點的判斷函式
def has_element_child(nodename):
    has_element_child = 0
    for child in nodename.childNodes:
        if child.nodeType==1:
            has_element_child += 1
    return has_element_child
# 定義解析示例XML檔案的方法
def match_xml(old_file,new_file):
    if not new_file:
        sys.exit(0)
    tree_old = parse(old_file) # document型別的解析樹
    tree_new = parse(new_file)
    root_old = tree_old.getElementsByTagName(`proxool`)[0] # 將父節點定位到proxool
    root_new = tree_new.getElementsByTagName(`proxool`)[0]
    old_dict = {} # 定義舊XML檔案的tag和data的字典
    new_dict = {}
    for child in root_old.childNodes: #將tagName和data存入old_dict{}中
        if child.nodeType==child.ELEMENT_NODE and has_element_child(child)==0: # 當node為element型別,且無element型別的子節點時
            old_dict[child.tagName] = child.firstChild.data.replace("
", "").replace("	", "")
    for child in root_new.childNodes:
        if child.nodeType==child.ELEMENT_NODE and has_element_child(child)==0:
            new_dict[child.tagName] = child.firstChild.data.replace("
", "").replace("	", "")
    for tag,data in new_dict.items():
        if not old_dict.get(tag):  # 當舊XML中找不到對應的tag時,進行tag新增操作
            new_element=tree_new.getElementsByTagName(tag)
            for child in new_element:
                root_old.appendChild(child) # 新增element節點
    with open(`proxool_modified.xml`,`w`) as f:
        tree_old.writexml(f)
        f.close
# 處理示例XML檔案
match_xml(old_file,new_file)
--比較XML檔案:
# python xml_match_dom.py proxool.xml proxool.xml.new
--然後就可以在proxool_modified.xml中看到新的XML內容了

相關文章