2024年4月24日 星期三

Python 學習筆記 : 用 xmltodict 模組讀寫 .xml 檔

我在 "超圖解 Python 程式設計入門" 這本書中找到一個 Python 第三方的套件 xmltodict, 可讀取 XML 文件並轉成 Python 字典, 在擷取 XML 文件資訊上非常方便, 比起 Python 內建 xml 套件的 ElementTree 模組要簡單多了. 關於 ElementTree 模組用法參考 :


首先用 pip install 安裝套件 :

D:\Python\test>pip install xmltodict   
Collecting xmltodict
  Downloading xmltodict-0.13.0-py2.py3-none-any.whl (10.0 kB)
Installing collected packages: xmltodict
Successfully installed xmltodict-0.13.0 

>>> import xmltodict   
>>> xmltodict.__version__   
'0.13.0'

用 dir() 檢視套件內容 : 

>>> dir(xmltodict)   
['AttributesImpl', 'ParsingInterrupted', 'StringIO', 'XMLGenerator', '_DictSAXHandler', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__license__', '__loader__', '__name__', '__package__', '__spec__', '__version__', '_basestring', '_dict', '_emit', '_process_namespace', '_unicode', 'expat', 'isgenerator', 'parse', 'platform', 'unparse']

使用下列自訂模組 members.py 來檢視其內容 :

# members.py
import inspect 
def varname(x): 
    return [k for k,v in inspect.currentframe().f_back.f_locals.items() if v is x][0]
def list_members(parent_obj):
    members=dir(parent_obj)
    parent_obj_name=varname(parent_obj)       
    for mbr in members:
        child_obj=eval(parent_obj_name + '.' + mbr) 
        if not mbr.startswith('_'):
            print(mbr, type(child_obj))  

將此函式存成 members.py 模組, 放在目前供作目錄下, 然後匯入其 list_members() 函式來檢視 xmltodict 模組 : 

>>> from members import list_members   
>>> list_members(xmltodict)   
AttributesImpl <class 'type'>
ParsingInterrupted <class 'type'>
StringIO <class 'type'>
XMLGenerator <class 'type'>
expat <class 'module'>
isgenerator <class 'function'>
parse <class 'function'>
platform <class 'module'>
unparse <class 'function'>

我們會用到的函式只有兩個 : 
  • parse(file) : 讀取 .xml 檔轉成字典傳回
  • unparse(dict) :  將字典轉成 XML 字串
以下仍以之前測試 xml 套件的顯示卡 XML 文件 display_card.xml 為例 :

<?xml version="1.0" encoding="UTF-8"?>
<顯示卡>
  <型號>NVidia RTX3060
    <GPU核心編號>GA106-300</GPU核心編號>
  <CUDA核心數>3584</CUDA核心數>
  <TensorCores>112</TensorCores>
  <VRAM 單位="GB" DDR="DDR6">12G</VRAM>
  <ResizableBAR支援>有</ResizableBAR支援>
  </型號>
</顯示卡> 

用 with open() 以 utf-8 編碼開啟 .xml 檔, 讀取檔案內容後傳給 xmltodict.parse(), 它會將 XML 文件剖析為一個字典傳回 : 

>>> with open('display_card.xml', encoding='utf-8') as f:  
    data=xmltodict.parse(f.read())  
    
>>> data   
{'顯示卡': {'型號': {'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060'}}}
>>> type(data)    
<class 'dict'>

這樣就用字典的存取方式很容易取得 XML 裡面的資料了, 例如 : 

>>> data['顯示卡']   
{'型號': {'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060'}}
>>> data['顯示卡']['型號']   
{'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060'}
>>> data['顯示卡']['型號']['GPU核心編號']   
'GA106-300'

可見無屬性的標籤會以其內容當作值; 有屬性的標籤, 其值為一個字典, 屬性的鍵會以 @ 開頭後面跟著屬性名稱, 而內容的鍵則固定為 #text. 

也可以呼叫字典物件的方法 :

>>> data['顯示卡']['型號'].keys()    # 傳回鍵串列
dict_keys(['GPU核心編號', 'CUDA核心數', 'TensorCores', 'VRAM', 'ResizableBAR支援', '#text'])
>>> data['顯示卡']['型號'].values()   # 傳回值串列
dict_values(['GA106-300', '3584', '112', {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, '有', 'NVidia GTX3060'])
>>> data['顯示卡']['型號'].items()     # 傳回鍵值對串列
dict_items([('GPU核心編號', 'GA106-300'), ('CUDA核心數', '3584'), ('TensorCores', '112'), ('VRAM', {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}), ('ResizableBAR支援', '有'), ('#text', 'NVidia GTX3060')])

新增標籤可以呼叫字典的 update({key, value}) 方法或 setdefault(key, value) :

>>> data['顯示卡']['型號'].update({"重量": "996g"})   
>>> data['顯示卡']['型號']    
{'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060', '重量': '996g'}

如果是有屬性的標籤, 則字典的值也是一個字典, 屬性在名稱前冠 @ 為鍵; 內容則以 #text 為鍵 :

>>> data['顯示卡']['型號'].update({"功耗": {"@單位": "瓦", "#text": "170W"}})    
>>> data['顯示卡']['型號']    
{'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060', '重量': '996g', '功耗': {'@單位': '瓦', '#text': '170W'}}

下面使用 set_default(key, value) 來新增節點 :

>>> data['顯示卡']['型號'].setdefault('HDCP支援', "有")    
'有'
>>> data['顯示卡']['型號']   
{'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060', '重量': '996g', '功耗': {'@單位': '瓦', '#text': '170W'}, 'HDCP支援': '有'}

如果標籤有屬性, 則 value 就傳入一個字典, 屬性在名稱前冠 @ 為鍵; 標籤的內容則固定以 #text 為鍵 :

>>> data['顯示卡']['型號'].setdefault('核心時脈', {'@單位': 'MHz', '#text': '1777'})   
{'@單位': 'MHz', '#text': '1777'}
>>> data['顯示卡']['型號']   
{'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060', '重量': '996g', '功耗': {'@單位': '瓦', '#text': '170W'}, 'HDCP支援': '有', '核心時脈': {'@單位': 'MHz', '#text': '1777'}}

呼叫 unparse() 函式並傳入字典可將其轉回 XML 文件, 語法如下 :

xmltodict.unparse(dict, [encoding='utf-8', full_document=True, pretty=False])  

參數說明如下 :
  • encoding : 預設為 utf-8 編碼
  • full_document : 輸出完整 XML 文件, 預設為 True
  • pretty : 是否將輸出文件, 預設 False
檢視目前修改後的 data 字典 :

>>> data   
{'顯示卡': {'型號': {'GPU核心編號': 'GA106-300', 'CUDA核心數': '3584', 'TensorCores': '112', 'VRAM': {'@單位': 'GB', '@DDR': 'DDR6', '#text': '12G'}, 'ResizableBAR支援': '有', '#text': 'NVidia GTX3060', '重量': '996g', '功耗': {'@單位': '瓦', '#text': '170W'}, 'HDCP支援': '有', '核心時脈': {'@單位': 'MHz', '#text': '1777'}}}}

為了讓輸出格式化, 匯入 pprint.pprint() 函式 :

>>> from pprint import pprint    
>>> xml_str=xmltodict.unparse(data)   
>>> type(xml_str)   
<class 'str'>  
>>> pprint(xml_str)   
('<?xml version="1.0" encoding="utf-8"?>\n'
 '<顯示卡><型號><GPU核心編號>GA106-300</GPU核心編號><CUDA核心數>3584</CUDA核心數><TensorCores>112</TensorCores><VRAM '
 '單位="GB" '
 'DDR="DDR6">12G</VRAM><ResizableBAR支援>有</ResizableBAR支援><重量>996g</重量><功耗 '
 '單位="瓦">170W</功耗><HDCP支援>有</HDCP支援><核心時脈 單位="MHz">1777</核心時脈>NVidia '
 'GTX3060</型號></顯示卡>')

傳入 pretty=True 會輸出格式化字串 : 

>>> xml_str=xmltodict.unparse(data, pretty=True)    
>>> pprint(xml_str)     
('<?xml version="1.0" encoding="utf-8"?>\n'
 '<顯示卡>\n'
 '\t<型號>\n'
 '\t\t<GPU核心編號>GA106-300</GPU核心編號>\n'
 '\t\t<CUDA核心數>3584</CUDA核心數>\n'
 '\t\t<TensorCores>112</TensorCores>\n'
 '\t\t<VRAM 單位="GB" DDR="DDR6">12G</VRAM>\n'
 '\t\t<ResizableBAR支援>有</ResizableBAR支援>\n'
 '\t\t<重量>996g</重量>\n'
 '\t\t<功耗 單位="瓦">170W</功耗>\n'
 '\t\t<HDCP支援>有</HDCP支援>\n'
 '\t\t<核心時脈 單位="MHz">1777</核心時脈>\n'
 'NVidia GTX3060\t</型號>\n'
 '</顯示卡>')

用 with open() 將轉換結果寫入檔案 :

>>> with open('display_card_5.xml', 'w', encoding='utf-8') as f:   
    f.write(xml_str)   
    
332

開啟檔案 display_card_5.xml 內容如下 : 

<?xml version="1.0" encoding="utf-8"?>
<顯示卡>
<型號>
<GPU核心編號>GA106-300</GPU核心編號>
<CUDA核心數>3584</CUDA核心數>
<TensorCores>112</TensorCores>
<VRAM 單位="GB" DDR="DDR6">12G</VRAM>
<ResizableBAR支援>有</ResizableBAR支援>
<重量>996g</重量>
<功耗 單位="瓦">170W</功耗>
<HDCP支援>有</HDCP支援>
<核心時脈 單位="MHz">1777</核心時脈>
NVidia GTX3060 </型號>
</顯示卡>

這樣就把字典轉換成 XML 文件了, 比用 xml 的 ElementTree 模組要簡單許多. 

沒有留言 :