可延伸標記語言
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 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'>
其中兩個成員可以用來建立子節點 :
有三個成員可用來建立 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() 檢查節點有幾個子節點, 每個節點是像串列, 可用 [] 以數字索引取得子節點 :
>>> 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 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'))
可以用迴圈走訪這個串列 :
>>> 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 的子節點 |
>>> 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支援版本></型號>
</顯示卡>
確認這兩個子節點已被刪除.
參考 :