2026年8月31日 星期一

Python一下:從XML讀懂智慧設備設定與地方導覽資料

《Python一下:從風土資料到智慧生活》第 23 篇

從XML讀懂智慧設備設定與地方導覽資料

從樹狀標記中找出水井三寶多語內容與設備參數,同時守住格式、來源及操作安全。

CH12 XMLElementTree設定驗證
學習目標
完成本篇後,你能辨認XML元素、屬性、文字與階層,使用 ElementTree 解析、搜尋、建立及輸出XML,處理命名空間與解析錯誤,並把設備設定轉成「待人工確認」的安全資料。

一、XML與JSON有何不同?

兩者都能表示結構化資料。JSON常見於網頁API;XML在設備設定、標準文件、RSS及舊系統交換格式中仍很常見。

特性JSONXML
主要結構物件、陣列、基本型別元素樹、屬性、文字
資料型別數字、布林、null等讀出時多半先是字串
重複節點通常使用陣列重複同名元素
命名空間沒有內建機制可避免不同詞彙名稱衝突
設備安全原則:XML格式正確不代表參數安全。教學程式只讀取、驗證並產生預覽;不可把外部XML直接送往水泵、曝氣機或其他設備。實際套用須確認來源、版本、簽章或完整性、場域規則及人工授權。

二、看懂XML結構

<guide version="1.0">
  <item id="white-horse" language="zh-Hant">
    <title>白馬</title>
    <status>待社區確認</status>
  </item>
  <item id="turtle" language="zh-Hant">
    <title>烏龜</title>
    <status>待社區確認</status>
  </item>
</guide>

guide 是根元素;item 是子元素;idlanguage 是屬性;title 元素內的「白馬」是文字內容。XML必須只有一個根元素,開始與結束標籤要正確巢狀。

三、從字串解析XML

import xml.etree.ElementTree as ET

xml_text = """
<guide version="1.0">
  <item id="marriage-flower" language="zh-Hant">
    <title>姻緣花</title>
    <status>待社區確認</status>
  </item>
</guide>
"""

root = ET.fromstring(xml_text)
print("根元素:", root.tag)
print("版本:", root.get("version"))

fromstring() 回傳根元素。XML屬性可用 get() 取得,找不到時回傳 None

四、搜尋元素與讀取文字

for item in root.findall("item"):
    item_id = item.get("id")
    language = item.get("language")
    title = item.findtext("title", default="").strip()
    status = item.findtext("status", default="").strip()

    print(item_id, language, title, status)
方法用途
find()尋找第一個符合的子元素
findall()取得所有符合元素
findtext()直接取得第一個符合元素的文字
iter()走訪目前元素下所有指定節點

五、從檔案安全解析

import xml.etree.ElementTree as ET
from pathlib import Path


def load_xml(path, max_bytes=2_000_000):
    path = Path(path)
    if not path.is_file():
        raise FileNotFoundError(f"找不到XML:{path}")
    if path.stat().st_size > max_bytes:
        raise ValueError("XML超過教學程式允許大小")

    try:
        tree = ET.parse(path)
    except ET.ParseError as error:
        raise ValueError(f"XML格式錯誤:{error}") from error
    return tree

檔案大小限制可降低意外資源消耗,但不是完整的XML安全防護。若要解析來源不可信的XML,應採用專為防禦XML攻擊設計的函式庫(如defusedxml)及環境資源限制。

六、把導覽元素轉成字典

def parse_guide_item(item):
    item_id = (item.get("id") or "").strip()
    language = (item.get("language") or "").strip()
    title = item.findtext("title", default="").strip()
    status = item.findtext("status", default="").strip()

    missing = [
        name
        for name, value in {
            "id": item_id,
            "language": language,
            "title": title,
            "status": status,
        }.items()
        if not value
    ]
    if missing:
        raise ValueError("缺少內容:" + "、".join(missing))

    return {
        "id": item_id,
        "language": language,
        "title": title,
        "status": status,
    }

XML沒有自動幫你驗證業務規則。讀取後仍須確認必要屬性、空白文字、重複ID及可接受的語言代碼。

七、批次處理並隔離錯誤

def parse_guide(root):
    records = []
    errors = []
    used_ids = set()

    for index, item in enumerate(root.findall("item"), start=1):
        try:
            record = parse_guide_item(item)
            if record["id"] in used_ids:
                raise ValueError(f"重複id:{record['id']}")
            used_ids.add(record["id"])
            records.append(record)
        except ValueError as error:
            errors.append({"序號": index, "原因": str(error)})

    return records, errors

八、解析智慧設備設定

device_xml = """
<device-config version="1.0" apply="preview-only">
  <device id="TEMP-01" type="temperature">
    <enabled>true</enabled>
    <sample-seconds>60</sample-seconds>
  </device>
</device-config>
"""

config_root = ET.fromstring(device_xml)
device = config_root.find("device")

device_id = device.get("id")
device_type = device.get("type")
enabled_text = device.findtext("enabled", default="").strip().lower()
sample_seconds = int(
    device.findtext("sample-seconds", default="")
)

print(device_id, device_type, enabled_text, sample_seconds)

XML文字須自行轉成 int 或布林值。不要使用 bool("false"),因為非空字串會得到 True

九、嚴格轉換布林與整數

def parse_boolean(text, field_name):
    normalized = str(text).strip().lower()
    if normalized == "true":
        return True
    if normalized == "false":
        return False
    raise ValueError(f"{field_name}只能是true或false")


def parse_positive_int(text, field_name, maximum):
    value = int(str(text).strip())
    if not 1 <= value <= maximum:
        raise ValueError(
            f"{field_name}必須介於1到{maximum}"
        )
    return value


enabled = parse_boolean(enabled_text, "enabled")
interval = parse_positive_int(
    sample_seconds, "sample-seconds", 86400
)
print(enabled, interval)
上限只是教學防線:86400秒並非真實設備管理建議。實際採樣週期與控制參數要由設備規格、場域人員與專業需求共同決定。

十、命名空間不是多餘的括號

XML命名空間避免不同標準使用相同標籤時衝突。ElementTree搜尋時要提供前綴對照。

import xml.etree.ElementTree as ET

xml_text = """
<g:guide xmlns:g="https://example.edu/guide">
  <g:item id="white-horse">
    <g:title>白馬</g:title>
  </g:item>
</g:guide>
"""

root = ET.fromstring(xml_text)
ns = {"g": "https://example.edu/guide"}

for item in root.findall("g:item", ns):
    title = item.findtext("g:title", default="", namespaces=ns)
    print(item.get("id"), title)

範例網址只是命名空間識別字,不代表瀏覽器一定能開啟該網址。

十一、建立XML導覽檔

import xml.etree.ElementTree as ET
from pathlib import Path


def build_guide_xml(records):
    root = ET.Element("guide", {"version": "1.0"})
    for record in records:
        item = ET.SubElement(
            root,
            "item",
            {
                "id": record["id"],
                "language": record["language"],
            },
        )
        ET.SubElement(item, "title").text = record["title"]
        ET.SubElement(item, "status").text = record["status"]

    tree = ET.ElementTree(root)
    ET.indent(tree, space="  ")
    return tree


records = [
    {"id": "white-horse", "language": "zh-Hant",
     "title": "白馬", "status": "待社區確認"},
]
tree = build_guide_xml(records)

由ElementTree建立元素時,特殊字元會被正確跳脫;不要用字串拼接XML,否則文字中的 &< 等符號容易破壞格式。

十二、不覆蓋地輸出XML

from pathlib import Path


def write_xml_exclusive(tree, path):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    xml_bytes = ET.tostring(
        tree.getroot(),
        encoding="utf-8",
        xml_declaration=True,
    )
    with path.open("xb") as file:
        file.write(xml_bytes)
    return path


# write_xml_exclusive(tree, "導覽資料/水井三寶_v1.xml")

十三、設定變更比較

def compare_device_config(old_config, new_config):
    changes = {}
    keys = sorted(old_config.keys() | new_config.keys())

    for key in keys:
        old_value = old_config.get(key)
        new_value = new_config.get(key)
        if old_value != new_value:
            changes[key] = {
                "舊值": old_value,
                "新值": new_value,
            }
    return changes

變更報告應先交由設備負責人確認,再經測試環境驗證;不可由XML解析程式直接套用到正式場域。

十四、常見錯誤檢查表

現象原因修正
ParseError標籤未關閉或巢狀錯誤檢查錯誤位置與原始來源
find找不到元素路徑錯誤或有命名空間確認階層並提供ns對照
false被轉成True使用bool("false")明確比對true/false
中文輸出成亂碼編碼不一致輸出UTF-8並寫入XML宣告
設定檔直接控制設備缺少驗證與核准層只產生預覽與變更報告

十五、USR實作挑戰

挑戰A|水井三寶多語XML
為白馬、烏龜與姻緣花建立華語、台語文字節點,加入確認狀態與授權欄位,找出缺漏及重複ID。
挑戰B|設備設定預覽
解析三個匿名設備設定,比較新舊版本,輸出變更報告,但不連接或操作任何真實設備。
挑戰C|錯誤XML測試
準備缺標籤、缺屬性、錯誤布林、過大整數與命名空間版本,確認程式能提供清楚錯誤。

十六、與生成式AI協作

請擔任Python XML與智慧設備設定安全助教。
請檢查我的ElementTree程式:
1. 驗證根元素、版本、必要屬性與文字;
2. 數字與布林必須嚴格轉型;
3. 支援XML命名空間;
4. 不以字串拼接XML;
5. 輸出UTF-8且不得覆蓋舊檔;
6. 不可信XML須限制大小並說明安全解析方案;
7. 設定只能產生預覽和變更報告,不可直接控制設備。
請提供正常與故障測試資料。
本篇小結
XML是一棵帶有元素、屬性與文字的樹。解析只是第一步;真正可靠的流程還要驗證結構、型別、命名空間、來源與版本,設備設定更必須經人工核准。

沒有留言:

張貼留言