2024年4月22日 星期一

Python 學習筆記 : 用 ElementTree 套件讀寫 .xml 檔

可延伸標記語言 XML (eXtensible Markup Language) 是簡化自 SGML (Standard Generalized Markup Language) 的一個標記語言, 是一種機器與人類皆可讀, 以 unicode 編碼的純文字資料交換格式. XML 與 HTML 最大的差異是 XML 的標籤可自訂且可從名稱理解資訊內容, 而 HTML 的標籤則是 W3C 所制定, 無法從標籤名稱理解資訊內容. 

XML 語法摘要如下 :
  • XML 是用來標記資料的結構的格式.
  • XML 檔案以 .xml 為副檔名, 第一行須聲明版本與編碼格式 :

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

  • XML 檔支援任何 Unicode 編碼,  utf-8 格式為大部分剖析器接受.
  • XML 標籤名稱有分大小寫 (HTML 不分) 且不可有空格.
  • 標籤可以多層嵌套. 起始標籤內可以有屬性. 
  • 資料存放的位置可在屬性與其值,  或者是子標籤
例如下面的顯示卡 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支援>
  </型號>
</顯示卡> 

將此 XML 碼以 utf-8 編碼格式存在工作目錄下的 display_card.xml, 然後用 Python 內建套件 xml 來讀取並剖析. xml 套件由多層子模組構成, 我們用來剖析 XML 文件的其實是 xml.etree 下的 ElementTree 模組, 教學文件參考 :


參考書籍 :



一. 剖析與走訪 XML 文件的語法樹 : 

首先來檢視 xml 套件之結構 : 

>>> import xml   
>>> dir(xml)   
['__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'etree']  
>>> type(xml.etree)  
<class 'module'> 

可見 xml 套件底下只有一個模組 etree, 用 dir() 檢視其內容 :

>>> dir(xml.etree)  
['ElementPath', 'ElementTree', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

可見 xml.etree 底下又有兩個模組 :

>>> type(xml.etree.ElementTree)    
<class 'module'>
>>> type(xml.etree.ElementPath)   
<class 'module'>  

此處會用到的是 ElementTree 這個子模組, 所以通常會直接匯入此模組並取簡名為 ET :

>>> import xml.etree.ElementTree as ET   

用 dir() 檢視其內容 :

>>> dir(ET)   
['C14NWriterTarget', 'Comment', 'Element', 'ElementPath', 'ElementTree', 'HTML_EMPTY', 'PI', 'ParseError', 'ProcessingInstruction', 'QName', 'SubElement', 'TreeBuilder', 'VERSION', 'XML', 'XMLID', 'XMLParser', 'XMLPullParser', '_Element_Py', '_ListDataStream', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_escape_attrib', '_escape_attrib_c14n', '_escape_attrib_html', '_escape_cdata', '_escape_cdata_c14n', '_get_writer', '_looks_like_prefix_name', '_namespace_map', '_namespaces', '_raise_serialization_error', '_serialize', '_serialize_html', '_serialize_text', '_serialize_xml', '_set_factories', 'canonicalize', 'collections', 'contextlib', 'dump', 'fromstring', 'fromstringlist', 'indent', 'io', 'iselement', 'iterparse', 'parse', 're', 'register_namespace', 'sys', 'tostring', 'tostringlist', 'warnings']

使用下列自訂模組 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() 函式來檢視 ElementTree 模組 : 

>>> from members import list_members 
>>> list_members(ET)   
>>> list_members(xml.etree.ElementTree)
C14NWriterTarget <class 'type'>
Comment <class 'function'>
Element <class 'type'>
ElementPath <class 'module'>
ElementTree <class 'type'>
HTML_EMPTY <class 'set'>
PI <class 'function'>
ParseError <class 'type'>
ProcessingInstruction <class 'function'>
QName <class 'type'>
SubElement <class 'builtin_function_or_method'>
TreeBuilder <class 'type'>
VERSION <class 'str'>
XML <class 'function'>
XMLID <class 'function'>
XMLParser <class 'type'>
XMLPullParser <class 'type'>
canonicalize <class 'function'>
collections <class 'module'>
contextlib <class 'module'>
dump <class 'function'>
fromstring <class 'function'>
fromstringlist <class 'function'>
indent <class 'function'>
io <class 'module'>
iselement <class 'function'>
iterparse <class 'function'>
parse <class 'function'>
re <class 'module'>
register_namespace <class 'function'>
sys <class 'module'>
tostring <class 'function'>
tostringlist <class 'function'>
warnings <class 'module'>

其中兩個成員可以用來建立子節點 :
  • Element 類別
  • SubElement 類別
有三個成員可用來建立 XML 語法樹以便進行資料剖析 : 
  • ElementTree 類別 : 呼叫其建構式 ElementTree() 會傳回 ElementTree 物件
  • parse() 函式 : 傳回 ElementTree 物件 (整棵 XML 語法樹)
  • fromstring() 函式 : 傳回 XML 語法樹的根節點 (Element 物件)
除了 fromstring() 函式是傳入 XML 字串外, parse() 與 ElementTree() 都是傳入 .xml 檔的路徑與檔名. 注意, fromstring() 傳回的不是代表整棵 XML 語法樹的 ElementTree 物件 , 而是它底下的根節點 Element 物件.  


1. 使用 parse() 函式剖析 XML 文件 :  

parse() 函式用來載入外部 .xml 檔案並剖析為 XML 語法樹, 傳入值為 .xml 文件的路徑與檔名, 傳回值為一個 ElementTree 物件, 語法如下 :

tree=ET.parse(xml_file)    

當呼叫 parse() 函式載入 XML 文件後會將其剖析為一棵由 ElementTree 物件與若干 Element 物件組成的 XML 語法樹, 以上面的顯示卡 XML 文件 display_card.xml 為例 :
 
>>> import xml.etree.ElementTree as ET   # 匯入 ElementTree 模組
>>> tree=ET.parse('display_card.xml')   
>>> type(tree)    
<class 'xml.etree.ElementTree.ElementTree'>   

這個 ElementTree 物件其實就是 XML 語法樹, 代表了整份 XML 文件. 用 dir() 檢視此 ElementTree 物件之內容 : 

>>> dir(tree)   
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_root', '_setroot', 'find', 'findall', 'findtext', 'getroot', 'iter', 'iterfind', 'parse', 'write', 'write_c14n']

用 list_members() 可以得知哪些是函式哪些是類別 :

>>> list_members(tree)   
find <class 'method'>
findall <class 'method'>
findtext <class 'method'>
getroot <class 'method'>
iter <class 'method'>
iterfind <class 'method'>
parse <class 'method'>
write <class 'method'>
write_c14n <class 'method'>

其中 getroot() 函式會傳回 XML 文件的根結點, 是一個 Element 物件 : 

>>> root=tree.getroot()    
>>> root 
<Element '顯示卡' at 0x00000214737AB600>
>>> type(root)   
<class 'xml.etree.ElementTree.Element'>

用 dir() 檢視 Element 物件內容 : 

>>> dir(root)   
['__class__', '__copy__', '__deepcopy__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'attrib', 'clear', 'extend', 'find', 'findall', 'findtext', 'get', 'insert', 'items', 'iter', 'iterfind', 'itertext', 'keys', 'makeelement', 'remove', 'set', 'tag', 'tail', 'text']

用 list_members() 可以得知哪些是函式哪些是類別 :

>>> list_members(root)   
append <class 'builtin_function_or_method'>
attrib <class 'dict'>
clear <class 'builtin_function_or_method'>
extend <class 'builtin_function_or_method'>
find <class 'builtin_function_or_method'>
findall <class 'builtin_function_or_method'>
findtext <class 'builtin_function_or_method'>
get <class 'builtin_function_or_method'>
insert <class 'builtin_function_or_method'>
items <class 'builtin_function_or_method'>
iter <class 'builtin_function_or_method'>
iterfind <class 'builtin_function_or_method'>
itertext <class 'builtin_function_or_method'>
keys <class 'builtin_function_or_method'>
makeelement <class 'builtin_function_or_method'>
remove <class 'builtin_function_or_method'>
set <class 'builtin_function_or_method'>
tag <class 'str'>
tail <class 'NoneType'>
text <class 'str'>

Element 物件常用屬性如下表 :


 Element 物件常用屬性 說明
 tag 標籤名稱 (值為字串)
 attrib 標籤屬性 (值為字典)
 text 標籤內容 (值為字串)


Element 物件常用方法如下表 :


 Element 物件常用方法 說明
 find(tag) 搜尋標籤名稱 tag, 傳回第一個 Element 物件
 findall(tag) 搜尋標籤名稱 tag, 傳回所有 Element 物件之串列
 iter(tag) 搜尋標籤名稱 tag, 傳回所有 Element 物件之迭代器
 keys() 傳回 Element 物件之全部屬性名稱 (串列)
 items() 傳回 Element 物件之全部屬性名稱與值元組的串列
 get(attr) 傳回 Element 物件之屬性 attr 之值
 set(attr, value) 設定 Element 物件之屬性 attr 之值為 value
 append(subelement) 在目前節點後面加上子節點 subelement
 extend(subelements) 在目前節點後面添加子節點串列 subelements
 remove(element) 刪除子節點物件 element


Element 物件是 XML 文件的基本組成元素, 整個 XML 文件被解析為一個 ElementTree 語法樹,  其最上層節點稱為樹根 (root), 底下由許多 Element 物件節點組成 : 




這跟 BeautifulSoup 將 HTML 文件解析成一棵 DOM 語法樹類似, 樹物件 ElementTree 相當於 BeautifulSoup 物件, 而 Element 物件則類似於 Tag 物件. 關於 BeautifulSoup 套件參考 : 


以上面的顯示卡 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支援>
  </型號>
</顯示卡> 

>>> import xml.etree.ElementTree as ET   
>>> tree=ET.parse('display_card.xml')   # TreeElement 物件=整棵 XML 語法樹
>>> root=tree.getroot()    # 根節點 : <顯示卡> 標籤

可用 len() 檢查節點有幾個子節點, 每個節點是像串列, 可用 [] 以數字索引取得子節點 :

>>> len(root)   
1
>>> root[0]   
<Element '型號' at 0x0000021473802430>

可見根節點 <顯示卡> 只有 1 個子節點 <型號>, 用 len() 繼續檢查可知它有 5 個子節點 :

>>> len(root[0])   
5  

可以用 2 維索引繼續往下取得下一層節點 :

>>> root[0][0]   
<Element 'GPU核心編號' at 0x0000021473802020>
>>> root[0][1]  
<Element 'CUDA核心數' at 0x00000214738025C0>
>>> root[0][2]  
<Element 'TensorCores' at 0x0000021473802070>
>>> root[0][3]  
<Element 'VRAM' at 0x0000021473802110>
>>> root[0][4]  
<Element 'ResizableBAR支援' at 0x0000021473802390>

用 for 迴圈走訪這 5 個子節點, 並用 tag, attrib, 與 text 屬性取得標籤名稱, 屬性字典, 以及標籤內容 :

>>> for item in root[0]:  
 print(item.tag, item.attrib, item.text)   
                    
GPU核心編號 {} GA106-300
CUDA核心數 {} 3584
TensorCores {} 112
VRAM {'單位': 'GB', 'DDR': 'DDR6'} 12G
ResizableBAR支援 {} 有

可見 5 個子節點中只有 <VRAM> 有屬性, 其它節點的 attrib 屬性值均為空字典. 

除了使用索引走訪 XML 語法樹, 還可以呼叫 Element 物件的 find(), findall(), 與 iter() 等方法來搜尋該節點下的子節點, 例如 root 物件是 <顯示卡>, root[0] 是 <型號>, 呼叫 root[0] 的 find(tag) 方法會傳回 <型號> 下的子節點 (Element 物件) : 

>>> root[0].find('CUDA核心數')   
<Element 'CUDA核心數' at 0x00000214738025C0>
>>> root[0].find('VRAM')     
<Element 'VRAM' at 0x0000021473802110>

find() 的傳回值是第一個符合之 Element 物件, 因此可用 text 屬性取得該物件之內容, 用 attrib 屬性取得其屬性字典 : 

>>> root[0].find('VRAM').text   
'12G'
>>> root[0].find('VRAM').attrib   
{'單位': 'GB', 'DDR': 'DDR6'}

findall() 與 iter() 都是搜尋所有子節點中特定之標籤名稱, 但傳回值不同, findall() 將全部符合之 Element 物件放在串列中傳回; 而 iter() 則是傳回搜尋結果的迭代器 (iterator), 必須用迴圈走訪才能取得這些 Element 物件, 當搜尋結果很龐大時可改用 iter() 來節省 DRAM 耗用情形, 例如搜尋 'VRAM' 標籤 : 

>>> root[0].findall('VRAM')   
[<Element 'VRAM' at 0x0000021473802110>]

左右兩邊有中括號表示傳回值是串列, 且只有一個元素, 可以用索引 0 取得 : 

>>> type(root[0].findall('VRAM'))   
<class 'list'>  
>>> root[0].findall('VRAM')[0]    
<Element 'VRAM' at 0x0000021473802110>
>>> root[0].findall('VRAM')[0].text     
'12G'
>>> root[0].findall('VRAM')[0].attrib    
{'單位': 'GB', 'DDR': 'DDR6'}

Element 物件的 get() 函式則是用來搜尋屬性值, 傳入參數是屬性名稱, 例如 :

>>> root[0].find('VRAM')     
<Element 'VRAM' at 0x0000021473802110
>>> root[0].find('VRAM').get('單位')    # 傳回 VRAM 標籤 '單位' 屬性之值 
'GB'
>>> root[0].find('VRAM').get('DDR')    # 傳回 VRAM 標籤 'DDR' 屬性之值
'DDR6'

下面參考 "xml.etree.ElementTree 筆記" 這篇文章中的 country_data.xml 文件比較 find(), findall(), 與 iter() 這三個方法的差異 :

<?xml version="1.0"?>
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
    <country name="Singapore">
        <rank>4</rank>
        <year>2011</year>
        <gdppc>59900</gdppc>
        <neighbor name="Malaysia" direction="N"/>
    </country>
    <country name="Panama">
        <rank>68</rank>
        <year>2011</year>
        <gdppc>13600</gdppc>
        <neighbor name="Costa Rica" direction="W"/>
        <neighbor name="Colombia" direction="E"/>
    </country>
</data>

首先用 ElementTree 模組的 parse() 函式讀檔並剖析為 ElementTree 語法樹物件 : 
 
>>> import xml.etree.ElementTree as ET   
>>> tree=ET.parse('country_data.xml')   
>>> root=tree.getroot()   
>>> root  
<Element 'data' at 0x0000021473803650>   

根節點是 <data> 標籤, 它底下有 3 個國家資訊的 <country> 標籤, 呼叫 find() 搜尋 'country' 標籤只能找到第一個國家 Liechtenstein :

>>> root.find('country')  
<Element 'country' at 0x0000021473803330>   
>>> root.find('country').tag   
'country'
>>> root.find('country').get('name')    # 取得屬性 name 之值
'Liechtenstein'

如果使用 findall() 搜尋 'country' 標籤就能找到全部 country 標籤之 Element 物件, 將它們放在串列中傳回來 :

>>> root.findall('country')   
[<Element 'country' at 0x0000021473803330>, <Element 'country' at 0x00000214738030B0>, <Element 'country' at 0x0000021473803920>]  
>>> type(root.findall('country'))   
<class 'list'>

可以用迴圈走訪這個串列 :

>>> for country in root.findall('country'):   
    print(country.attrib)   
                    
{'name': 'Liechtenstein'}
{'name': 'Singapore'}
{'name': 'Panama'}

如果要擷取 XML 文件中的國名, 排行, 與人均 GDP 可以這麼寫 :

>>> for country in root.findall('country'):  
    name=country.get('name')  
    rank=country.find('rank').text  
    gdppc=country.find('gdppc').text  
    print(f'name:{name} rank:{rank} gdppc:{gdppc}')  
                    
name:Liechtenstein rank:1 gdppc:141100
name:Singapore rank:4 gdppc:59900
name:Panama rank:68 gdppc:13600

呼叫 iter() 同樣是搜尋符合之所有子節點, 但傳回一個迭代器 :

>>> root.iter('country')   
<_elementtree._element_iterator object at 0x0000021473828680> 

可以用迴圈走訪迭代器的每個元素 :

>>> for country in root.iter('country'):  
    name=country.get('name')  
    rank=country.find('rank').text  
    gdppc=country.find('gdppc').text  
    print(f'name:{name} rank:{rank} gdppc:{gdppc}')  
                    
name:Liechtenstein rank:1 gdppc:141100
name:Singapore rank:4 gdppc:59900
name:Panama rank:68 gdppc:13600

結果與使用 findall() 相同, 但使用迭代器在資料很大時較不佔用記憶體, 關於迭代器參考 :



2. 呼叫 ElementTree 類別的建構式剖析 XML 文件 : 

第二種剖析 XML 文件的方法是呼叫 xml.etree.ElementTree.ElementTree 類別的建構函式  ElementTree() 並傳入 .xml 檔之路徑與檔名, 它會傳回一個 ElementTree 物件 :

>>> import xml.etree.ElementTree as ET   

以上面的顯示卡 XML 文件 display_card.xml 為例, 呼叫 ElementTree() 並傳入此 XML 文件就會讀取它並將其剖析為一個 XML 語法樹 :

>>> tree=ET.ElementTree(file='display_card.xml')   
>>> type(tree)    
<class 'xml.etree.ElementTree.ElementTree'>  
>>> root=tree.getroot()   
>>> type(root)  
<class 'xml.etree.ElementTree.Element'>
>>> root  
<Element '顯示卡' at 0x0000021473802160>  
>>> root.tag  
'顯示卡'

其它操作均與上面 parse() 相同. 


3. 呼叫 fromstring() 函式剖析 XML 文件 : 

fromstring() 函式用來剖析 XML 字串, 首先將上面 display_card.xml 的內容放進一個長字串 : 

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

然後將此常字串傳給 ElementTree 模組的 fromstring() 函式. 注意, 與呼叫 parse() 不同的是, 它在剖析完成後傳回 XML 語法樹的根節點 Element 物件, 而非像 parse() 那樣傳回 ElementTree 物件 (整棵語法樹) : 

>>> root=ET.fromstring(xml_str)     
>>> type(root)   
<class 'xml.etree.ElementTree.Element'>   
>>> root  
<Element '顯示卡' at 0x0000021473802ED0>

其它操作均與上面 parse() 相同. 

當然也可以用 with open() 指定 utf-8 編碼格式開啟 .xml 檔, 呼叫 read() 讀取檔案內容字串之後再傳給 fromstring() 剖析為 XML 語法樹, 例如 : 

>>> with open('display_card.xml', 'r', encoding='utf-8') as f:   
    root=ET.fromstring(f.read())   
                    
>>> root   
<Element '顯示卡' at 0x0000021473803740>


二. 使用 XPath 搜尋節點 : 

XPath (XML Path Language) 是一種小型的路徑表達式, 用在呼叫 findall() 或 iter() 時於 XML 文件語法樹中搜尋目標節點. XPath 採用類似於檔案路徑的描述, 例如 /A/B/C 表示找尋根節點的子節點下的 C 節點, 而 A//B/*[1] 表示搜尋目前節點下的 A 節點下不論幾層下的子孫節點 B 的任何標籤名稱的第一個節點, 參考 :


不過 xml 套件並沒有支援全部的 XPath, 僅支援如下常用語法 : 


 xml 套件支援的 XPath 語法 說明
 tag 搜尋標籤名稱為 tag 的子節點
 * 所有子節點
 . 目前節點
 // 全部子孫節點
 .. 父節點 (上一層)
 [@attr] 搜尋全部含有屬性 attr 的節點
 [@attr='value'] 搜尋全部含有屬性 attr, 且值為 value 的節點
 [tag] 搜尋標籤名稱為 tag 的所有子節點
 [tag='text'] 搜尋全部子孫節點中標籤名稱為 tag, 且內容為 text 的所有子節點
 [index] 搜尋位置索引 (1 起始) 為 index 的子節點


以上面的 country_data.xml 文件為例 : 


>>> import xml.etree.ElementTree as ET   
>>> tree=ET.parse('country_data.xml')   
>>> root=tree.getroot()   
>>> root  
<Element 'data' at 0x0000021473803650>   

呼叫 findall() 時傳入 "." 表示要尋找目前節點 : 

>>> root.findall(".")   
[<Element 'data' at 0x0000021473803650>]

可見目前是在根節點 <data> 下. 傳入 "./country/neighbor" 表示要尋找目前節點下的 country 下的全部 neighbor 節點 :

>>> root.findall("./country/neighbor")   
[<Element 'neighbor' at 0x0000021473802CA0>, <Element 'neighbor' at 0x0000021473803970>, <Element 'neighbor' at 0x00000214738036F0>, <Element 'neighbor' at 0x0000021473803BA0>, <Element 'neighbor' at 0x0000021473803B50>]

可見 5 個 <neighbor> 節點都找到了. 傳入 ".//neighbor" 表示要搜尋語法樹中的所有 <neighbor> 標籤, ".//" 表示目前節點 <data> 的所有子孫節點 :

>>> root.findall(".//neighbor")  
[<Element 'neighbor' at 0x0000021473802CA0>, <Element 'neighbor' at 0x0000021473803970>, <Element 'neighbor' at 0x00000214738036F0>, <Element 'neighbor' at 0x0000021473803BA0>, <Element 'neighbor' at 0x0000021473803B50>]

同樣是這 5 個 <neighbor> 節點, 但意思與上面的 "./country/neighbor" 不同. 

傳入 "*[@name]" 表示要搜尋所有子節點 "*" 中含有 name 屬性者 : 

>>> root.findall("*[@name]")    
[<Element 'country' at 0x0000021473803330>, <Element 'country' at 0x00000214738030B0>, <Element 'country' at 0x0000021473803920>]

只找出 3 個 <country> 標籤, <neighbor> 節點雖然也有 name 屬性, 因為 <neighbor> 是孫節點, 而 "*" 表示只抓所有子節點而已. 

如果要抓全部子孫節點中含有 name 屬性者, 要傳入 ".//*[@name]", 其中 ".//" 表示現在節點下的所有子孫, "*[@name]" 表示含有 name 屬性的子節點:

>>> root.findall(".//*[@name]")   
[<Element 'country' at 0x0000021473803330>, <Element 'neighbor' at 0x0000021473802CA0>, <Element 'neighbor' at 0x0000021473803970>, <Element 'country' at 0x00000214738030B0>, <Element 'neighbor' at 0x00000214738036F0>, <Element 'country' at 0x0000021473803920>, <Element 'neighbor' at 0x0000021473803BA0>, <Element 'neighbor' at 0x0000021473803B50>]
>>> len(root.findall(".//*[@name]"))
8

可見連同 <country> 與 <neighbor> 共有 8 個標籤含有 name 屬性. 

傳入 ".//*[@name='Panama']/neighbor" 表示要搜尋目前節點的全部子孫中含有 name='Panama' 屬性的 <neighbor> 子節點, 這會找到兩個 :

>>> root.findall(".//*[@name='Panama']/neighbor")  
[<Element 'neighbor' at 0x0000021473803BA0>, <Element 'neighbor' at 0x0000021473803B50>]


三. 操控 XML 語法樹與寫入 .xml 檔 : 

Element 物件的 get(attr) 可以取得屬性 attr 之值, set(attr, value) 則可設定其值, 例如 : 

>>> import xml.etree.ElementTree as ET   
>>> tree=ET.parse('display_card.xml')   
>>> root=tree.getroot()   
>>> root     
<Element '顯示卡' at 0x0000021473828540>   

用索引取得根節點 <顯示卡> 下第一個子節點 <型號>, 由 attr 屬性為空字典可知它原本是沒有屬性的, 呼叫 set() 幫它設定一個屬性 "重量" 後就有了 : 

>>> root[0]     
<Element '型號' at 0x0000021473828DB0>   
>>> root[0].attr  
{}  
>>> root[0].set("重量", "996g")    
>>> root[0].get("重量")   
'996g'  
>>> root[0].attrib  
{'重量': '996g'}

呼叫 keys 會把全部屬性名稱 (key) 以串列傳回, 呼叫 items() 則會把 (屬性, 值) 以串列傳回 : 

>>> root[0].keys()   
['重量']
>>> root[0].items()   
[('重量', '996g')]

呼叫 append() 可以將一個節點貼附到目前節點後面, 首先用 ElementTree 的 Element 類別來建立一個節點, 只要呼叫其建構式 Element() 並傳入標籤名稱即可, 例如欲在 RTX3060 顯示卡的 XML 文件上附加 <功耗>170W</功耗> 節點 :

>>> power=ET.Element("功耗")   # 呼叫建構式傳入標籤名稱
>>> power   
<Element '功耗' at 0x000002147384C1D0>   
>>> power.text="170W"    # 設定標籤內容
>>> power.set("單位", "瓦")     # 設定屬性
>>> power.items()     # 傳回屬性串列
[('單位', '瓦')]   

然後呼叫 <型號> 標籤的 Element 物件 (即 root[0]) 的 append() 方法將此 <功耗> 標籤加到最後一個子節點後面 :

>>> root.find('型號').append(power)    # 用 root[0].append(power) 也可以 
>>> len(root[0].findall('.//'))     # <型號> 的子節點多了 1 個
6
>>> root[0].findall('.//')  
[<Element 'GPU核心編號' at 0x0000021473828630>, <Element 'CUDA核心數' at 0x000002147382B5B0>, <Element 'TensorCores' at 0x0000021473828220>, <Element 'VRAM' at 0x0000021473828360>, <Element 'ResizableBAR支援' at 0x0000021473803010>, <Element '功耗' at 0x000002147384C1D0>]

可見多出了新增的 <功耗> 子節點.

將此修改過的 ElementTree 物件 tree 寫到新的 ,xml 檔, 這可以透過呼叫 ElementTree 物件的 write() 方法並指定 encoding='utf-8' 屬性即可將整棵 XML 語法樹寫入 .xml 檔, 語法如下 : 

tree.write("路徑檔名", encoding="utf-8")  

>>> tree.write('display_card_1.xml', encoding='utf-8')   

這時開啟 display_card_1.xml 檔 : 

<顯示卡>
  <型號>NVidia GTX3060
    <GPU核心編號>GA106-300</GPU核心編號>
<CUDA核心數>3584</CUDA核心數>
<TensorCores>112</TensorCores>
<VRAM 單位="GB" DDR="DDR6">12G</VRAM>
<ResizableBAR支援>有</ResizableBAR支援>
  <功耗 單位="瓦">170W</功耗></型號>
</顯示卡>

可見 <型號> 確實多了一個子節點 <功耗>, 

新增節點也可以使用 SubElement 類別, 呼叫其建構式 SubElement(element, tag) 即可在 element 節點後面添加子節點 <tag>, 由於建構式第一參數已指定父節點物件, 因此不需要使用父節點的 append() 方法來添加, 例如 : 

>>> hdcp=ET.SubElement(root[0], 'HDCP支援')     # 在 <型號> 節點下建立子節點
>>> hdcp.text='有'    # 設定子節點內容
>>> hdcp   
<Element 'HDCP支援' at 0x000002147475E1B0>    
>>> root[0].findall('.//')    
[<Element 'GPU核心編號' at 0x0000021473828630>, <Element 'CUDA核心數' at 0x000002147382B5B0>, <Element 'TensorCores' at 0x0000021473828220>, <Element 'VRAM' at 0x0000021473828360>, <Element 'ResizableBAR支援' at 0x0000021473803010>, <Element '功耗' at 0x000002147384C1D0>, <Element 'HDCP支援' at 0x000002147475E1B0>]

將目前的語法樹寫入 display_card_2.xml 檔 :

>>> tree.write('display_card_2.xml', encoding='utf-8')   

開啟此檔案內容如下 :

<顯示卡>
  <型號>NVidia GTX3060
    <GPU核心編號>GA106-300</GPU核心編號>
<CUDA核心數>3584</CUDA核心數>
<TensorCores>112</TensorCores>
<VRAM 單位="GB" DDR="DDR6">12G</VRAM>
<ResizableBAR支援>有</ResizableBAR支援>
  <功耗 單位="瓦">170W</功耗><HDCP支援>有</HDCP支援></型號>
</顯示卡>

可見 <型號> 節點下已新增了一個子節點 <HDCP支援>. 

另外一個可添加子節點的是 extends(elements) 方法, 與 append() 方法一次只能添加一個子節點不同的是, 它可一次添加多個子節點, 其傳入值為 Element 物件之串列, 裡面可以放多個要添加的子節點 :  

先用 ET.Element() 建構式建立兩個子節點 :

>>> directx=ET.Element("DIRECTX支援版本")   
>>> directx.text="12"   
>>> opengl=ET.Element("OPENGL支援版本")   
>>> opengl.text="4.6"   

再將此兩子節點放入串列或元組中傳給父節點的 extend() 方法將其加入父節點中 :

>>> root[0].extend([directx, opengl])   
>>> root[0].findall('.//')   
[<Element 'GPU核心編號' at 0x0000021473828630>, <Element 'CUDA核心數' at 0x000002147382B5B0>, <Element 'TensorCores' at 0x0000021473828220>, <Element 'VRAM' at 0x0000021473828360>, <Element 'ResizableBAR支援' at 0x0000021473803010>, <Element '功耗' at 0x000002147384C1D0>, <Element 'HDCP支援' at 0x000002147475E1B0>, <Element 'DIRECTX支援版本' at 0x000002147475E160>, <Element 'OPENGL支援版本' at 0x000002147475E070>]

將目前的語法樹寫入 display_card_3.xml 檔 :

>>> tree.write('display_card_3.xml', encoding='utf-8')   

開啟檔案檢視內容 :

<顯示卡>
  <型號>NVidia GTX3060
    <GPU核心編號>GA106-300</GPU核心編號>
<CUDA核心數>3584</CUDA核心數>
<TensorCores>112</TensorCores>
<VRAM 單位="GB" DDR="DDR6">12G</VRAM>
<ResizableBAR支援>有</ResizableBAR支援>
  <功耗 單位="瓦">170W</功耗><HDCP支援>有</HDCP支援><DIRECTX支援版本>12</DIRECTX支援版本><OPENGL支援版本>4.6</OPENGL支援版本></型號>
</顯示卡>

可見這兩個子節點已加入至父節點 <型號> 內. 

最後來測試 Element 物件的 remove(element) 方法, 它可刪除傳入之子節點, 接續上面結果, 若要刪除 <功耗> 與 <HDCP支援> 子節點, 可以用 find() 搜尋後傳入 remove() : 

>>> root[0].remove(root[0].find('功耗'))   
>>> root[0].remove(root[0].find('HDCP支援'))      
>>> root[0].findall('.//')   
[<Element 'GPU核心編號' at 0x0000021473828630>, <Element 'CUDA核心數' at 0x000002147382B5B0>, <Element 'TensorCores' at 0x0000021473828220>, <Element 'VRAM' at 0x0000021473828360>, <Element 'ResizableBAR支援' at 0x0000021473803010>, <Element 'DIRECTX支援版本' at 0x000002147475E160>, <Element 'OPENGL支援版本' at 0x000002147475E070>]

將 tree 寫入檔案 : 

>>> tree.write('display_card_4.xml', encoding='utf-8')    

開啟 display_card_4.xml 內容如下 :

<顯示卡>
  <型號>NVidia GTX3060
    <GPU核心編號>GA106-300</GPU核心編號>
<CUDA核心數>3584</CUDA核心數>
<TensorCores>112</TensorCores>
<VRAM 單位="GB" DDR="DDR6">12G</VRAM>
<ResizableBAR支援>有</ResizableBAR支援>
  <DIRECTX支援版本>12</DIRECTX支援版本><OPENGL支援版本>4.6</OPENGL支援版本></型號>
</顯示卡>

確認這兩個子節點已被刪除. 

參考 : 


沒有留言:

張貼留言