💡 完整可運作的範例可在 GitHub 上取得:
document-version-metadata-diff-python
您將建構的內容
在本指南中,您將比較兩個文件版本之間的每個 metadata 屬性,並精確列印出新增、移除或變更的內容。metadata 版本差異是對同一檔案兩個修訂的屬性層級比較,能捕捉文字比較永遠看不到的訊號:新的 Creator、提升的 RevisionNumber、在審閱關閉後記錄的編輯會話。完成後,您將擁有一個可運作的解決方案,外加兩個專注的偵測器與兩種匯出格式,全部取自一個已備有範例修訂對的可執行儲存庫。
技能等級:中階 Python 開發者
需求:Python 3、pip,以及同一文件的兩個修訂版本
我第一次執行此腳本時,偵測到一個團隊成員都不記得更改過的 Company 值變動;那一行程式碼就為設定付出了代價。以下內容皆可直接複製貼上,總行數遠低於一百行。
此流程刻意保持簡單:兩次檔案開啟、三個 dict 推導式、一個列印迴圈。簡單正是重點。版本爭議的裁決在於方法是否可說明且可重現,而這麼小的腳本能讓任何挑戰結果的人完整閱讀。
1. 安裝
pip install groupdocs-metadata-net==26.5
伴隨的儲存庫已固定此版本,並提供 document-v1.docx 與 document-v2.docx,因此下方程式碼可直接執行。請以您審計時使用的版本為基準固定版本;可重現性是證據的一部份。
2. 核心程式碼
先讀取兩個屬性樹,然後以集合邏輯分類差異。這就是完整的 diff:
# Flatten a file's complete property tree into a dict
def read_props(path):
props = {}
with Metadata(path) as metadata:
for p in metadata.find_properties(lambda p: p.name is not None):
props[p.name] = (str(p.interpreted_value) if p.interpreted_value is not None
else (str(p.value) if p.value is not None else ""))
return props
v1 = read_props("resources/document-v1.docx")
v2 = read_props("resources/document-v2.docx")
# Classify every key; changed entries keep both values
added = {k: v for k, v in v2.items() if k not in v1}
removed = {k: v for k, v in v1.items() if k not in v2}
changed = {k: (v1[k], v2[k]) for k in v1 if k in v2 and v1[k] != v2[k]}
print(f"added={len(added)} removed={len(removed)} changed={len(changed)}")
for k, (old_v, new_v) in changed.items():
print(f" {k}: {old_v} -> {new_v}")
這就是您所需的最小程式碼。對於真正的修訂對,通常只會出現少量差異;若差異達到數十筆,通常表示檔案在途中經過模板變更或儲存遷移。接下來的章節會說明關鍵呼叫,並展示大多數團隊最先加入的客製化。
3. 工作原理
Metadata:負責開啟檔案並在離開時釋放資源的 context manager;每個修訂使用一個實例。find_properties:一次遍歷內建欄位、自訂屬性與 XMP,回傳符合 predicate 的所有項目。interpreted_value:屬性的可讀形式;優先使用它可讓日期與列舉以字串形式比較,便於在報告中列印。- 以合格名稱作為鍵:內建與自訂欄位不會在 dict 中衝突,因而集合邏輯保持安全。
此處不會解析 DOCX 結構。產品文件列出 170 多種格式皆可透過相同呼叫處理,因此相同腳本亦可比較 PDF 或 XLSX 配對。
設計上還有一點值得說明:API 邊界止於兩個 read_props 呼叫。其後的所有程式碼皆為標準函式庫 Python,因而單元測試、閾值與警示規則永遠不會觸及文件層。將此腳本包裝成服務的團隊通常會為每個修訂快取已抽出的 dict,讓所有下游檢查重複使用,無論問多少問題,都只會對每個版本執行一次檔案開啟。
4. 常見客製化
僅偵測所有權變更
當問題是「是誰觸碰了這個檔案」時,可在讀取階段使用標籤 predicate 直接過濾,而不是在完整 diff 後再過濾:
# Identity fields only, whatever the format calls them
def read_ownership(path):
result = {}
with Metadata(path) as metadata:
props = metadata.find_properties(lambda p:
Tags.person.creator in list(p.tags)
or Tags.person.editor in list(p.tags)
or Tags.person.manager in list(p.tags)
or Tags.corporate.company in list(p.tags))
for prop in props:
result[prop.name] = (str(prop.interpreted_value)
if prop.interpreted_value is not None
else (str(prop.value) if prop.value is not None else ""))
return result
對兩個此類 dict 執行相同的差異迴圈,使用 <missing> 作為預設值,讓消失的欄位仍能被顯示。predicate 不指定具體欄位名稱,正因如此,一個偵測器即可服務所有庫可讀的格式。
追蹤編輯時間線
將 predicate 換成 Tags.time 加上名稱規則,即可讓偵測器回報 RevisionNumber、TotalEditingTime 與 LastPrinted 的變動:
props = metadata.find_properties(lambda p:
Tags.time.modified in list(p.tags)
or Tags.time.created in list(p.tags)
or Tags.time.printed in list(p.tags)
or (p.name is not None and ("Revision" in p.name
or "EditTime" in p.name or "EditingTime" in p.name)))
匯出稽核報告
只在主控台列印的結果會隨即消失。四欄格式即可滿足試算表與 SIEM 案件需求:
with open("output/diff.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["change_type", "property", "old_value", "new_value"])
for k, v in added.items():
writer.writerow(["added", k, "", v])
for k, v in removed.items():
writer.writerow(["removed", k, v, ""])
for k, (old_v, new_v) in changed.items():
writer.writerow(["changed", k, old_v, new_v])
儲存庫亦提供 JSON 匯出器,採用穩定的三層映射結構,方便儀表板與案件管理 API 使用。
5. 實務上的執行情境
目前常見的三種部署方式:
- 入口管線:每當新文件抵達時,與已存檔的副本做 diff,將身份變更的配對隔離。
- 合規工作:排程執行 diff,並將每對 CSV 存檔,形成屬性時間線,之後無需再重建。
- 爭議工具:按需執行兩個偵測器,因為當爭議發生時,第一個問題永遠是「誰何時觸碰了檔案」,而非段落四的文字變更。
第四種模式是將檔案與其最後一次已知良好快照做 diff,使用同樣程式碼,只是其中一側改為已儲存的 dict。於所有情境中,匯出檔案即為最終交付物;主控台輸出僅作為進度訊息。腳本的退出代碼遵循儲存庫的 main.py,因此排程器與 CI 可直接將斷言失敗視為執行失敗,無需額外設定。上述所有情境皆不需要超出本頁所示的程式碼。
6. 哪些變更值得標記?
任何 diff 所分類的項目加上您自行加入的上下文皆屬於值得關注的變更。新增與移除的屬性總是值得檢查,因為它們代表結構變更而非單純值變動。對於變更的條目,多數團隊會先對身份與修訂相關的群組發出警示,其餘則視為資訊性。偵測器的存在即是為了讓第一輪檢查只需一次函式呼叫。
7. 快速參考:關鍵呼叫
| 呼叫 | 功能說明 |
|---|---|
Metadata(path) |
開啟檔案;context manager 會在離開時釋放 |
find_properties(predicate) |
回傳所有符合 predicate 的屬性,涵蓋所有層級 |
p.interpreted_value |
可讀的值;若無則退回 p.value |
Tags.person.* / Tags.corporate.company |
身份分類,與格式無關 |
Tags.time.* |
時間戳分類,用於修訂偵測器 |
請參閱完整 API 參考文件,了解全部搜尋與標籤功能。標籤詞彙遠超此表;來源、內容與法律標籤群組皆遵循相同的成員測試方式。
8. 常見問題與解決方案
Diff 結果過於龐大,像噪音
→ 兩個路徑可能不是同一文件的修訂。解決方法:在 diff 前驗證來源;不相關的檔案會產生毫無意義的差異。
所有權偵測器永遠找不到已知的作者欄位
→ 某些產生器會將身份資訊存於未標記的自訂欄位。解決方法:先執行完整 diff,找出實際欄位名稱,然後在 predicate 中加入名稱規則。
主控台顯示 evaluation-mode 警告
→ 未找到授權檔。解決方法:在 main.py 中將 LICENSE_PATH 指向您的 .lic 檔,或在開發階段保留 evaluation 模式;邏輯完全相同。
日期以原始序列號顯示
→ 某處不小心使用了 p.value。解決方法:在 read_props 中保持 interpreted_value 為首選,這是報告保持可讀性的關鍵。
9. 下一步?
您已擁有可運作的 metadata diff。以下是可進一步採取的行動:
- 批次處理:將腳本迴圈化,對文件對組產生 CSV;每對的成本僅為兩次檔案開啟,CSV 可直接串接形成全庫視圖。
- 排程執行:儲存庫的
main.py會在每一步斷言並回傳正確的退出代碼,直接嵌入 CI 或排程系統。 - 走訪教學版本:參考使用案例指南,該指南以三個階段式教學建構相同管線。
- 檢視完整專案:document-version-metadata-diff-python 內含已備好的修訂對。