比較兩份 XML 文件的結構與內容差異是測試、版本控制、配置同步的常見需求。本文基於標準庫 `xml.etree.ElementTree` 實作遞迴比較器,檢測標籤差異、文本差異、屬性增減、子節點數量與順序差異。
from xml.etree import ElementTree
OK = True
MAIN_PID = 10000
LOOP_DEPTH = 0
def compare_xml(left: ElementTree.Element, right: ElementTree.Element, key_info: str = '.'):
global LOOP_DEPTH, OK
LOOP_DEPTH += 1
if LOOP_DEPTH == 1:
print() # 頂層換行
# 1. 標籤比較
if left.tag != right.tag:
print_diff(MAIN_PID, key_info, 'difftag', left.tag, right.tag)
LOOP_DEPTH -= 1
return
# 2. 文本內容比較
if (left.text or '').strip() != (right.text or '').strip():
print_diff(MAIN_PID, key_info, 'difftext', left.text, right.text)
# 不 return,繼續比較子節點
# 3. 屬性比較
left_attrs = dict(left.items())
right_attrs = dict(right.items())
for k, v in left_attrs.items():
if k not in right_attrs:
path = f'{key_info}/{left.tag}'
print_diff(MAIN_PID, path, 'lostattr', k, '')
for k, v in right_attrs.items():
if k not in left_attrs:
path = f'{key_info}/{right.tag}'
print_diff(MAIN_PID, path, 'extraattr', '', k)
# 4. 子節點比較 (按順序配對)
left_children = list(left)
right_children = list(right)
if len(left_children) != len(right_children):
path = f'{key_info}/{right.tag}'
print_diff(MAIN_PID, path, 'difflen', len(left_children), len(right_children))
LOOP_DEPTH -= 1
return
# 同名子節點計數器,處理同名同級節點
tag_counter = {}
for i in range(len(left_children)):
lchild = left_children[i]
rchild = right_children[i]
# 產生唯一路徑標識
if lchild.tag not in tag_counter:
tag_counter[lchild.tag] = 1
tag_path = lchild.tag
else:
tag_counter[lchild.tag] += 1
tag_path = f'{lchild.tag}{tag_counter[lchild.tag]}'
child_path = f'{key_info}/{tag_path}'
compare_xml(lchild, rchild, child_path)
LOOP_DEPTH -= 1
def print_diff(pid: int, key_info: str, msg: str, base_val, test_val):
global OK
info = f'[ {msg.upper():-5s} ] {pid} -> {key_info.strip("./"):-40s} [ {base_val} != {test_val} ]'
print(info)
OK = False
if __name__ == '__main__':
xml1 = '''
linux
30
windows
20
'''
xml2 = '''
windows
20
linux
30
'''
left_root = ElementTree.fromstring(xml1)
right_root = ElementTree.fromstring(xml2)
compare_xml(left_root, right_root)
if OK:
print("XML 內容完全一致")
else:
print("XML 存在差異 (見上方詳細輸出)")
[ DIFFTAG ] 10000 -> employees/employee1 [ employee != employee ]
[ LOSTATTR ] 10000 -> employees/employee1 [ id != ]
[ EXTRAATTR ] 10000 -> employees/employee1 [ != id ]
[ DIFFTEXT ] 10000 -> employees/employee1/name [ linux != windows ]
[ DIFFTEXT ] 10000 -> employees/employee1/age [ 30 != 20 ]
[ DIFFTEXT ] 10000 -> employees/employee2/name [ windows != linux ]
[ DIFFTEXT ] 10000 -> employees/employee2/age [ 20 != 30 ]
| 代碼 | 含義 |
|---|---|
| `difftag` | 同層級對應位置標籤名不同 |
| `difftext` | 節點文本內容不同 (已 strip 空白) |
| `lostattr` | 左側有、右側無此屬性 |
| `extraattr` | 右側有、左側無此屬性 |
| `difflen` | 子節點數量不同 |