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 模組要簡單許多. 

高科大還書 1 本

因為預約書到館, 所以拿下面這本去換 :


此書我幾乎看完了, 只剩最後兩章迴歸與分類的尚未做筆記, 有空再借回來補寫. 

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 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支援版本></型號>
</顯示卡>

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

參考 : 


2024 年第 16 周記事

上週日將鄉下老家那台開不了機的舊 PC (二哥國高中時玩遊戲改裝的) 載來高雄, 但一直拖到周六才拿去順發檢修, 帥哥工程師檢查後發現應該是其中兩排有散熱片的 DDR3 記憶體有問題所致, 另兩排金士頓的一個 OK, 另一個 NG, 因為有終生保固, 所以幫我送原廠更換. 另外 ASUS 獨顯無輸出判定應該是壞了, 並建議我買新的機殼, 他幫我挑了一個 1090 元有 LED 的, 先付機殼的錢後我就先回家了. 今天下班後去取貨, 帥哥已經幫我先安裝 Win10 隨機版 (CPU 無法上 Win11), 先用那排 OK 的 4GB DRAM 跑 (目前插在 CPU 旁起算第二排), 等金士頓保固送回來再插第一排即可 (優先是抓第二排). 檢修與裝機費合計 1000 元, 花 2100 救回一台電腦也還 OK, 當備援的下載機. 

鄉下的小灰周二生出三隻小貓咪, 原先是在車庫鐵架後面冷氣下方的小空間養子, 但周六一隻隻刁到庫房門口椅子下的紙箱內, 我週六下午回到家趁小灰不在把其中一隻抓出來照相 :







不過當她聽到小貓仔喵喵叫走過來, 雖然只在旁邊看沒阻止我, 但過一會兒她就把他們一隻隻又刁走, 後來發現是移到庫房裡面的角落了, 可能是怕我偷她小孩, 哈哈. 

水某週五去新加坡開會, 姊姊與菁菁原本要跟, 但姐姐周六要出差回高雄, 因她老闆接到藥廠臨床試驗研討會的案子, 要在義享天地的萬豪酒店舉辦, 所以只好取消, 這回水某自己一個人去 (我陪她去也是可以, 但新加坡太小, 1.5 天也不知要去哪玩, 花機票錢不值得). 不過時間也搭配得剛剛好, 水某週四晚上去台北住姐姐那裡一晚, 週五水某去搭機, 姊姊回高雄. 

本周看完 Netflix 的 "三體", 這原先是上次二哥回來掃墓時說想看第一集就好, 聽說此集有對文化大革命批鬥之寫實描述, 上週看完寄生獸接著就把三體也看完. 劇中那個遊戲頭盔好特別好時尚, 沒充電孔是透過無線充電嗎? 三體人的科技令人恐懼 (所以霍金才說別跟外星人有連繫). 淚之女神來到尾聲越來越精彩, 癡情又奸詐的尹殷盛為了讓洪海仁手術醒來失去記憶時第一時間看到他, 竟然偽造白賢祐殺害片姓男子證據, 導致白賢祐在海仁甦醒前遭到國際刑警逮捕, 這個周末將迎來完結篇, 期待白女婿的逆轉大作戰. 

2024年4月20日 星期六

Python 學習筆記 : 用 pickle 讀寫二進位檔

常見的資料交換檔案格式 csv, xml, 與 json 等都是以人類可讀之字串形式儲存, 可透過相關套件的編解碼功能讀取後轉成 Python 的資料型別. Python 內建一個 pickle 套件可用來將 Python 原生資料型態以序列化 (serialized) 的方式寫入二進位檔 (通常使用 .pickle 或 .dat 為副檔名), 讀取時則依序直接還原成 Python 物件, 稱為反序列化 (先進先出).  




二進位檔若以純文字編輯軟體開啟會看到一堆亂碼. 


1. 檢視 pickle 套件內容 : 

pickle 是 Python 內建標準函式庫,可直接匯入使用 : 

>>> import pickle

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

>>> list_members(pickle)   
ADDITEMS <class 'bytes'>
APPEND <class 'bytes'>
APPENDS <class 'bytes'>
BINBYTES <class 'bytes'>
BINBYTES8 <class 'bytes'>
BINFLOAT <class 'bytes'>
BINGET <class 'bytes'>
BININT <class 'bytes'>
BININT1 <class 'bytes'>
BININT2 <class 'bytes'>
BINPERSID <class 'bytes'>
BINPUT <class 'bytes'>
BINSTRING <class 'bytes'>
BINUNICODE <class 'bytes'>
BINUNICODE8 <class 'bytes'>
BUILD <class 'bytes'>
BYTEARRAY8 <class 'bytes'>
DEFAULT_PROTOCOL <class 'int'>
DICT <class 'bytes'>
DUP <class 'bytes'>
EMPTY_DICT <class 'bytes'>
EMPTY_LIST <class 'bytes'>
EMPTY_SET <class 'bytes'>
EMPTY_TUPLE <class 'bytes'>
EXT1 <class 'bytes'>
EXT2 <class 'bytes'>
EXT4 <class 'bytes'>
FALSE <class 'bytes'>
FLOAT <class 'bytes'>
FRAME <class 'bytes'>
FROZENSET <class 'bytes'>
FunctionType <class 'type'>
GET <class 'bytes'>
GLOBAL <class 'bytes'>
HIGHEST_PROTOCOL <class 'int'>
INST <class 'bytes'>
INT <class 'bytes'>
LIST <class 'bytes'>
LONG <class 'bytes'>
LONG1 <class 'bytes'>
LONG4 <class 'bytes'>
LONG_BINGET <class 'bytes'>
LONG_BINPUT <class 'bytes'>
MARK <class 'bytes'>
MEMOIZE <class 'bytes'>
NEWFALSE <class 'bytes'>
NEWOBJ <class 'bytes'>
NEWOBJ_EX <class 'bytes'>
NEWTRUE <class 'bytes'>
NEXT_BUFFER <class 'bytes'>
NONE <class 'bytes'>
OBJ <class 'bytes'>
PERSID <class 'bytes'>
POP <class 'bytes'>
POP_MARK <class 'bytes'>
PROTO <class 'bytes'>
PUT <class 'bytes'>
PickleBuffer <class 'type'>
PickleError <class 'type'>
Pickler <class 'type'>
PicklingError <class 'type'>
PyStringMap <class 'NoneType'>
READONLY_BUFFER <class 'bytes'>
REDUCE <class 'bytes'>
SETITEM <class 'bytes'>
SETITEMS <class 'bytes'>
SHORT_BINBYTES <class 'bytes'>
SHORT_BINSTRING <class 'bytes'>
SHORT_BINUNICODE <class 'bytes'>
STACK_GLOBAL <class 'bytes'>
STOP <class 'bytes'>
STRING <class 'bytes'>
TRUE <class 'bytes'>
TUPLE <class 'bytes'>
TUPLE1 <class 'bytes'>
TUPLE2 <class 'bytes'>
TUPLE3 <class 'bytes'>
UNICODE <class 'bytes'>
Unpickler <class 'type'>
UnpicklingError <class 'type'>
bytes_types <class 'tuple'>
codecs <class 'module'>
compatible_formats <class 'list'>
decode_long <class 'function'>
dispatch_table <class 'dict'>
dump <class 'builtin_function_or_method'>
dumps <class 'builtin_function_or_method'>
encode_long <class 'function'>
format_version <class 'str'>
io <class 'module'>
islice <class 'type'>
load <class 'builtin_function_or_method'>
loads <class 'builtin_function_or_method'>
maxsize <class 'int'>
pack <class 'builtin_function_or_method'>
partial <class 'type'>
re <class 'module'>
sys <class 'module'>
unpack <class 'builtin_function_or_method'>
whichmodule <class 'function'>

常用的函式如下表 : 


 pickle 常用函式 說明
 dump(obj, file) 將物件 obj 寫入二進檔 file 中 (序列化)
 dumps(obj) 將物件 obj 轉成二進位 (Byte 型別) 傳回
 load(file) 從二進檔 file 依序 (先進先出)  讀取物件傳回
 loads(byte) 從 Byte 型別資料讀取, 傳回物件


其中最常用的是 load() 與 dump() 函式. 注意, 讀寫 pickle 檔時須使用 "rb" 或 "wb" 模式開啟檔案


2. 讀寫 pickle 檔 : 

呼叫 pickle.dump() 可將物件依序寫入 pickle 檔, 呼叫 pickle.load() 則按先進先出順序讀取 pickle 檔內的物件, 例如 :

>>> import pickle   
>>> with open('test.pickle', 'wb') as f:   
    pickle.dump(123, f)    
    pickle.dump([True, False], f)   
    pickle.dump({'a':1, 'b': 2}, f)   

上面程式碼依序將 123, [True, False], 與 {'a': 1, 'b': 2} 這三個物件寫入二進位檔 test.pickle,  完成後會在目前工作目錄下建立一個 test.pickle 檔, 檢視內容會發現這三個物件占用了 51 個 bytes :




如果用記事本編輯將會看到一堆亂碼 :




呼叫 pickle.load() 則可從 pickle 檔依序讀回物件 (先進先出) : 

>>> with open('test.pickle', 'rb') as f:   
    obj1=pickle.load(f)    
    obj2=pickle.load(f)   
    obj3=pickle.load(f)   
    print(obj1, type(obj1))   
    print(obj2, type(obj2))   
    print(obj3, type(obj3))   
    
123 <class 'int'>
[True, False] <class 'list'>
{'a': 1, 'b': 2} <class 'dict'>

可見呼叫 load() 時是從 pickle 檔中按先進先出順序將儲存的物件讀出還原為 Python 物件. 

pickle 檔唯一的缺點是無法得知裡面儲存了多少物件, 因此讀到檔尾時會拋出 EOFError 例外,例如上面的 pickle 檔內存有 3 個物件, 若做 4 次讀取動作就會出現例外 : 

>>> with open('test.pickle', 'rb') as f:   
    obj1=pickle.load(f)    
    obj2=pickle.load(f)   
    obj3=pickle.load(f)   
    obj4=pickle.load(f)
    print(obj1, type(obj1))   
    print(obj2, type(obj2))   
    print(obj3, type(obj3))   
    print(obj4, type(obj4))
    
Traceback (most recent call last):
  File "<pyshell>", line 5, in <module>
EOFError: Ran out of input  

因此, 保險的作法是用 try catch 捕捉讀寫例外, 例如 :

>>> objs=[]   
>>> with open('test.pickle', 'rb') as f:   
    while True:   
        try:   
            obj=pickle.load(f)   
        except Exception as e:   
            print(f'載入 {len(objs)} 個物件')   
            break    
        print(obj, type(obj))     
        objs.append(obj)    
        
123 <class 'int'>
[True, False] <class 'list'>
{'a': 1, 'b': 2} <class 'dict'>
載入 3 個物件

此例使用無窮迴圈來走訪 pickle 檔裡面讀取到的物件, 並將其存入一個串列, 當讀到檔尾拋出例外時便結束迴圈並計算已讀取的物件數量. 


3. 讀寫 Byte 資料 : 

pickle.loads() 與 pickle.dumps() 可用來讀寫 Byte 型態資料, 例如 :

>>> b1=pickle.dumps(123)   
>>> b1   
b'\x80\x04K{.'    
>>> type(b1)    
<class 'bytes'>   
>>> b2=[True, False]   
>>> b2=pickle.dumps([True, False])    
>>> b2   
b'\x80\x04\x95\x07\x00\x00\x00\x00\x00\x00\x00]\x94(\x88\x89e.'   
>>> type(b2)   
<class 'bytes'>   
>>> b3=pickle.dumps({'a':1, 'b': 2})   
>>> b3   
b'\x80\x04\x95\x11\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01a\x94K\x01\x8c\x01b\x94K\x02u.'  
>>> type(b3)     
<class 'bytes'>     

呼叫 pickle.loads() 並傳入上面用 dumps() 得到的 Bytes 物件就可以將其還原為原生的物件 :

>>> pickle.loads(b1)  
123    
>>> type(pickle.loads(b1))   
<class 'int'>
>>> pickle.loads(b2)   
[True, False]
>>> type(pickle.loads(b2))   
<class 'list'>
>>> pickle.loads(b3)    
{'a': 1, 'b': 2}   
>>> type(pickle.loads(b3))      
<class 'dict'>  

2024年4月19日 星期五

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

最近在整理 Python 網頁爬蟲筆記, 覺得有兩個內建模組很常用, 第一個是 csv 模組, csv 檔是 Excel 試算表的純文字版, csv 模組提供讀寫 csv 檔的函式可快速地將檔案內容轉成 Python 字典或串列, 用法參考 : 


第二個是 json 模組, 這是 Python 用來處理 JSON 格式資料的內建模組, 與 csv 模組一樣, 直接匯入即可使用 :

import json  

JSON  (JavaScript Object Notation) 是一種結構化資料表示法, 源自 Javascript 的物件定義語法, 因為具有輕量與可讀性高的特性, 逐漸取代較複雜的 XML 成為最普及的 Web 資料交換格式, 也被許多程式語言支援, 很多 Web API 採用 JSON 格式來回傳, 很多 NoSQL 資料庫也以 JSON 來儲存資料, 參考 : 


JSON 資料格式有兩種語法, 第一種是與 Python 字典類似, 由鍵值對組成的 Javascript 物件, 語法如下 :

{"key1": "value1", "key2": "value2", ........}   
 
規則 : 
  • 鍵值對必須用大括號括起來, 每組鍵值對以逗號隔開
  • 鍵 (key) 必須是字串, 且須使用雙引號括起來, 不可用單引號
  • 值 (value) 可以是所有 Javascript 資料型態 : 數值, 字串, true/false, 陣列, null
  • 物件內不可使用任何註解
  • 交換資料時存成副檔名為 .json 檔案, 每一個 .json 檔只能含有一個物件 
例如 :

{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

第二種 JSON 資料表示法為 Javascript 陣列, 語法與 Python 串列一樣, 元素放在中括號裡面以逗號隔開, 可以是任何 Javascript 資料型態 :

[e1, e2, e3, ...]

例如 : 

["Tony", "male", 26, 172.5, true, null]

不論是陣列或物件, 在一個 JSON 檔裡面只能有一個陣列或一個物件, 如果有多個物件, 可以將其放入一個陣列裡成為物件陣列, 例如 : 

[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null},
 {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]

如果有多個陣列, 可以將其放入另一個陣列裡成為多維陣列, 例如 : 

[["Tony", "male", 26, 172.5, true, null], ["Jane", "female", 22, 167.2, false, null]]

注意, 這些 JSON 資料都是以字串形式做交換, json 模組提供了 loads() 與 dumps() 函式可用來在 JSON 字串與 Python 字典之間做轉換. 


1. 檢視 json 模組內容 :   

匯入 json 模組後可以用 dir() 函式檢視其內容 : 

>>> import json      
>>> dir(json)   
['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_default_decoder', '_default_encoder', 'codecs', 'decoder', 'detect_encoding', 'dump', 'dumps', 'encoder', 'load', 'loads', 'scanner']     

也可以用下列自訂模組 members 之 list_members() 函式來進一步了解那些是類別與函式 :

# 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() 函式來檢視 json 模組 : 

>>> from members import list_members    
>>> list_members(json)   
JSONDecodeError <class 'type'>
JSONDecoder <class 'type'>
JSONEncoder <class 'type'>
codecs <class 'module'>
decoder <class 'module'>
detect_encoding <class 'function'>
dump <class 'function'>
dumps <class 'function'>
encoder <class 'module'>
load <class 'function'>
loads <class 'function'>
scanner <class 'module'>

常用的函式如下表 :


 json 常用函式 說明
 load(file) 從檔案參考 file 讀取 JSON 資料為 Python 字典後傳回
 loads(str) 從 JSON 字串 str 讀取 JSON 資料為 Python 字典後傳回
 dump(data, file) 將 Python 字典寫入 .json 檔案參考 file
 dumps(data [, sort_keys, indent]) 將 Python 字典轉成 JSON 字串後傳回


這四個函式會在 JSON 字串與 Python 字典之間進行轉換, 注意有無 s 的差別在於, 有 s 的處理對象是 JSON 字串; 而無 s 的則是 ,json 檔案. 

Javascript 與 Python 資料類型之對照如下表 :


 JSON 資料類型 Python 資料類型
 物件 object 字典 dict
 陣列 array 串列 list
 字串 string 字串 str
 整數 int 整數 int
 浮點數 Number 浮點數 float
 布林值 true 布林值 True
 布林值 false 布林值 False
 空值 null 空值 None


注意, Javascript 的 true/false 對應於 Python 的 True/False, 而 Javascript 的 null 則對應於 Python 的 None. 


2. 呼叫 json.loads() 將 JSON 字串轉成 Python 字典/串列 :   

json.loads(str) 用來將 JSON 字串轉成 Python 字典, 傳入值為一個 JSON 字串, 傳回值為一個 Python 字典或串列, 例如 :   

>>> json_str='{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}'    
>>> obj=json.loads(json_str) 
>>> type(obj)   
<class 'dict'>
>>> obj    
{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}

可見 json.loads() 將 JSON 字串載入後轉成 Python 字典, 其值也轉成對應之 Python 資料型態. 

其次來看多個物件放在陣列的情況, 如上所述, JSON 資料中只能有一個 Javascript 物件, 若有多個物件要放在陣列中成為物件陣列 :

>>> json_str='[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}, {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]'     # JSON 資料為兩個物件組成之陣列
>>> obj=json.loads(json_str)     
>>> type(obj)   
<class 'list'>
>>> obj   
[{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}, {'name': 'Jane', 'gender': 'female', 'age': 22, 'height': 167.2, 'married': False, 'religion': None}]

可見 loads() 會將 JSON 字串轉成字典串列. 


3. 呼叫 json.dumps() 將 Python 字典/串列轉成 JSON 字串 :    

dumps() 是 loads() 的反函式, 它可將 Python 字典/串列轉成 JSON 字串, 例如 : 

>>> data={'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}   
>>> json_str=json.dumps(data)   
>>> type(json_str)   
<class 'str'>   
>>> json_str      
'{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}'

下面是將字典串列轉成 JSON 字串的測試 :

>>> data=[{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}, {'name': 'Jane', 'gender': 'female', 'age': 22, 'height': 167.2, 'married': False, 'religion': None}]   
>>> json_str=json.dumps(data)    
>>> json_str    
'[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}, {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]'

當物件多的時候會很難閱讀, 可用 indent 參數來調整縮排, 例如 : 

>>> json_str=json.dumps(data, indent=4)     
>>> json_str   
'[\n    {\n        "name": "Tony",\n        "gender": "male",\n        "age": 26,\n        "height": 172.5,\n        "married": true,\n        "religion": null\n    },\n    {\n        "name": "Jane",\n        "gender": "female",\n        "age": 22,\n        "height": 167.2,\n        "married": false,\n        "religion": null\n    }\n]'

要用 print() 輸出才看得到效果 :

>>> print(json_str)    
[
    {
        "name": "Tony",
        "gender": "male",
        "age": 26,
        "height": 172.5,
        "married": true,
        "religion": null
    },
    {
        "name": "Jane",
        "gender": "female",
        "age": 22,
        "height": 167.2,
        "married": false,
        "religion": null
    }
]


4. 呼叫 json.dump() 將 Python 字典/串列寫入 .json 檔案 :    

呼叫 json.dump() 並傳入 Python 字典/串列與檔案參考可將其寫入 json 檔案 (ANSI 編碼) : 

>>> data={'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}  
>>> with open('test.json', 'w') as f:  
    json.dump(data, f)   

開啟目前工作目錄下的 test.json 內容如下, 編碼格式為 ANSI :

{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

可見 dump() 已經將 true/false 轉成 True/False, None 轉成 null 了, 同時也把原先字典使用的單引號全部轉成雙引號, 因為 JSON 格式中字串必須使用雙引號. 

如果字典中所有非英文字母會轉成 unicode, 例如將 name 欄位值改成 "金秀賢" :  

{"name": "\u91d1\u79c0\u8ce2", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}


5. 呼叫 json.load() 載入 .json 檔案轉成 Python 字典/串列 :   

呼叫 json.load(file) 可以從檔案 file 讀取 JSON 資料, 它會傳回一個字/串列, 例如上面範例的檔案 test.json : 

{"name": "\u91d1\u79c0\u8ce2", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

>>> with open('test.json', 'r') as f:   
    data=json.load(f)   
                    
>>> data  
{'name': '金秀賢', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}