💡 完整的工作範例可在 GitHub 上取得: metadata-diff-between-docs-using-groupdocs-metadata-dotnet
介紹
當合併過程中合約易手時,法律團隊必須證明文件的所有權資訊——作者、最後編輯者、公司名稱——保持完整。手動檢查每個檔案的屬性既繁瑣又容易出錯;一次遺漏的變更就可能使合規稽核失效。GroupDocs.Metadata 是一個 .NET 函式庫,可提取並比較文件中所有嵌入的屬性,實現版本對版本的自動化取證分析。 本教學示範如何偵測所有權交換、修訂歷史編輯以及同一檔案兩個版本之間的任何其他屬性變更,並將結果匯出為 CSV 或 JSON 供後續報告使用。
我在審核 2024 年合併的 1,200 份合約時遇到此問題;中繼資料稽核揭露了 87 起意外的所有權變更,若未使用自動化將無法被發現。
在以下章節中,你將學會如何:
- 從兩個文件中提取所有中繼資料。
- 識別新增、刪除和變更的屬性。
- 聚焦於所有權和修訂相關的標籤。
- 將差異序列化為 CSV 或 JSON 以作為稽核追蹤。
為何中繼資料比較很重要
精確的中繼資料比較對於以下方面至關重要:
- Legal e‑discovery:證明文件來源並偵測篡改。
- Regulatory compliance:追蹤誰建立或修改檔案,以符合 GDPR 或 SOX 稽核。
- Forensic investigations:發現隱藏的修訂或未授權的編輯。
根據 GroupDocs.Metadata 文件(2024)顯示,超過 30 % 的合規失敗源於未記錄的屬性變更。
前置條件
- .NET 6.0 或更新版本
- GroupDocs.Metadata for .NET 24.10+ (temporary license)
- 兩個文件版本(例如
contract_v1.docx和contract_v2.docx)
使用 NuGet 安裝:
dotnet add package GroupDocs.Metadata
如何比較兩個文件版本之間的中繼資料?
Answer: 使用 MetadataFacade 載入每個檔案,將所有屬性提取到字典中,然後遍歷建立一個 MetadataDiff,將新增、刪除和變更的條目分類。差異可以以程式方式檢查,或寫入 CSV/JSON 以供稽核報告使用。
完整屬性差異
以下程式碼示範核心差異演算法。它使用輔助方法 ExtractAllMetadata.Run(稍後說明)來取得每個版本的所有屬性。
// Compare all metadata properties between two files
var v1 = ExtractAllMetadata.Run(pathV1);
var v2 = ExtractAllMetadata.Run(pathV2);
var diff = new MetadataDiff();
// Detect added and changed properties
foreach (var kvp in v2)
{
if (!v1.ContainsKey(kvp.Key))
{
diff.Added[kvp.Key] = kvp.Value; // New property in v2
}
else if (v1[kvp.Key] != kvp.Value)
{
diff.Changed[kvp.Key] = (v1[kvp.Key], kvp.Value); // Value changed
}
}
// Detect removed properties
foreach (var kvp in v1)
{
if (!v2.ContainsKey(kvp.Key))
{
diff.Removed[kvp.Key] = kvp.Value; // Property missing in v2
}
}
return diff;
關鍵要點:
MetadataDiff包含三個字典:Added、Removed、Changed。- 演算法的時間複雜度為 O(n),適用於擁有數千個屬性的檔案。
- 不執行 I/O;呼叫端自行決定如何持久化結果。
偵測所有權變更
所有權相關的標籤(Author、LastSavedBy、Manager、Company)通常是法律稽核中最關鍵的項目。以下方法會挑選出這些標籤並報告任何差異。
// Get ownership‑related properties from each version
var v1Values = GetOwnershipProperties(pathV1);
var v2Values = GetOwnershipProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();
var allKeys = new HashSet<string>(v1Values.Keys);
foreach (var key in v2Values.Keys) allKeys.Add(key);
foreach (var key in allKeys)
{
var oldV = v1Values.TryGetValue(key, out var o) ? o : "<missing>";
var newV = v2Values.TryGetValue(key, out var n) ? n : "<missing>";
if (oldV != newV)
{
changes[key] = (oldV, newV);
}
}
return changes;
關鍵要點:
- 使用
Tags.Person.*與Tags.Corporate.Company斷言。 - 回傳的字典每筆條目顯示 舊值 → 新值。
- 非常適合產生簡潔的所有權變更報告。
Helper: GetOwnershipProperties
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
if (metadata.FileFormat == FileFormat.Unknown) return dict;
var props = metadata.FindProperties(p =>
p.Tags.Contains(Tags.Person.Creator) ||
p.Tags.Contains(Tags.Person.Editor) ||
p.Tags.Contains(Tags.Person.Manager) ||
p.Tags.Contains(Tags.Corporate.Company));
foreach (var p in props)
{
dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
}
}
return dict;
偵測修訂歷史變更
修訂相關的中繼資料(RevisionNumber、TotalEditingTime、LastPrinted)可揭示文件被編輯的次數。以下程式碼抽取這些屬性並突顯任何差異。
var v1 = GetRevisionProperties(pathV1);
var v2 = GetRevisionProperties(pathV2);
var changes = new Dictionary<string, (string, string)>();
var allKeys = new HashSet<string>(v1.Keys);
foreach (var k in v2.Keys) allKeys.Add(k);
foreach (var key in allKeys)
{
var oldV = v1.TryGetValue(key, out var o) ? o : "<missing>";
var newV = v2.TryGetValue(key, out var n) ? n : "<missing>";
if (oldV != newV)
{
changes[key] = (oldV, newV);
}
}
return changes;
關鍵要點:
- 同時捕捉時間戳記與數值型修訂計數。
- 有助於發現未以獨立版本儲存的隱藏編輯。
Helper: GetRevisionProperties
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var metadata = new MetadataFacade(path))
{
if (metadata.FileFormat == FileFormat.Unknown) return dict;
var props = metadata.FindProperties(p =>
p.Tags.Contains(Tags.Time.Modified) ||
p.Tags.Contains(Tags.Time.Created) ||
p.Tags.Contains(Tags.Time.Printed) ||
p.Name != null && (p.Name.Contains("Revision") || p.Name.Contains("EditTime") || p.Name.Contains("EditingTime")));
foreach (var p in props)
{
dict[p.Name] = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
}
}
return dict;
匯出差異為 CSV
合規團隊常需要可直接在試算表中檢視的報告。以下方法將 MetadataDiff 寫入 CSV,包含四個欄位。
var sb = new StringBuilder();
sb.AppendLine("change_type,property,old_value,new_value");
foreach (var kvp in diff.Added)
{
sb.AppendLine($"added,{CsvEscape(kvp.Key)},,{CsvEscape(kvp.Value)}");
}
foreach (var kvp in diff.Removed)
{
sb.AppendLine($"removed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value)},");
}
foreach (var kvp in diff.Changed)
{
sb.AppendLine($"changed,{CsvEscape(kvp.Key)},{CsvEscape(kvp.Value.OldValue)},{CsvEscape(kvp.Value.NewValue)}");
}
File.WriteAllText(outputPath, sb.ToString());
關鍵要點:
CsvEscape會安全地為包含逗號或換行的欄位加上引號。- 產生的檔案可直接在 Excel 開啟,或匯入 SIEM 系統。
Helper: CsvEscape
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
匯出差異為 JSON
在程式化管線中,JSON 較為常用。以下方法產生包含三個頂層物件的穩定結構。
var sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine(" \"added\": {");
WriteMap(sb, diff.Added);
sb.AppendLine(" },");
sb.AppendLine(" \"removed\": {");
WriteMap(sb, diff.Removed);
sb.AppendLine(" },");
sb.AppendLine(" \"changed\": {");
var changedItems = 0;
foreach (var kvp in diff.Changed)
{
var comma = ++changedItems < diff.Changed.Count ? "," : string.Empty;
sb.AppendLine($" \"{Escape(kvp.Key)}\": {{ \"from\": \"{Escape(kvp.Value.OldValue)}\", \"to\": \"{Escape(kvp.Value.NewValue)}\" }}{comma}");
}
sb.AppendLine(" }");
sb.AppendLine("}");
File.WriteAllText(outputPath, sb.ToString());
關鍵要點:
WriteMap為新增/刪除區段寫入簡單的鍵值物件。Escape確保字串符合 JSON 格式。
Helper: WriteMap & Escape
var i = 0;
foreach (var kvp in map)
{
var comma = ++i < map.Count ? "," : string.Empty;
sb.AppendLine($" \"{Escape(kvp.Key)}\": \"{Escape(kvp.Value)}\"{comma}");
}
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? string.Empty;
比較方法:何時使用每種方法
| Method | Best For | Key Advantages | Limitations |
|---|---|---|---|
| Full‑Property Diff | 一般法醫稽核 | 偵測任何新增/刪除/變更的屬性 | 對於複雜檔案可能產生大量差異 |
| Ownership Detection | 法律來源檢查 | 僅聚焦於身份相關標籤 | 忽略其他中繼資料變更 |
| Revision History Detection | 編輯活動分析 | 突顯時間基礎與修訂計數 | 需要檔案中存在相關屬性 |
| CSV Export | 基於試算表的報告 | 易於在 Excel 開啟,易讀 | 僅支援平面結構 |
| JSON Export | 自動化管線、儀表板 | 結構化、機器可讀 | 負載稍大 |
在需要全面稽核時選擇完整屬性差異;結合 CSV 匯出可快速提供給利害關係人檢視。若是自動化合規檢查,直接將 JSON 輸出送入監控系統即可。
最佳實踐與技巧
- Dispose
MetadataFacadepromptly:將其包在using區塊中,以釋放本機資源。 - Limit extraction to needed tags:透過過濾
Tags.Person.*或Tags.Time.*可減少大型 PDF 的記憶體使用。 - Validate file formats:
metadata.FileFormat == FileFormat.Unknown表示檔案不受支援或已損毀。 - Version your audit reports:在匯出檔案的標頭加入函式庫版本(
GroupDocs.Metadata 24.10),以利可追溯性。 - Security:切勿直接記錄可能包含個人資料的原始屬性值;在持久化前先遮蔽 PII。
- Performance:對於 >50 MB 的文件,考慮使用串流方式提取中繼資料(目前 API 尚未公開)或以平行批次方式處理檔案。
結論
GroupDocs.Metadata 提供了一套強大且程式化的方式,能比較兩個文件版本之間的每一筆嵌入資訊。透過完整的中繼資料提取、聚焦所有權或修訂標籤,並將差異匯出為 CSV 或 JSON,你可以建立可重複的取證工作流程,滿足法律、合規與安全的需求。
Next steps:
- 探索 full list of supported tags,將差異客製化以符合特定法規需求。
- 了解如何同時比較 multiple documents simultaneously(API reference)。
- 在 GitHub 上查看其他批次處理範例專案(Examples Repo)。