💡 Full working example available on GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

Giới thiệu

Khi một hợp đồng được chuyển giao trong quá trình sáp nhập, đội ngũ pháp lý phải chứng minh rằng thông tin sở hữu tài liệu – tác giả, người chỉnh sửa cuối cùng, tên công ty – vẫn nguyên vẹn. Kiểm tra thủ công các thuộc tính của từng tệp là công việc tẻ nhạt và dễ gây lỗi; một thay đổi bị bỏ lỡ có thể làm mất hiệu lực của cuộc kiểm toán tuân thủ. GroupDocs.Metadata là một thư viện .NET giúp trích xuất và so sánh mọi thuộc tính nhúng của tài liệu, cho phép thực hiện phân tích pháp y tự động các thay đổi từ phiên bản này sang phiên bản khác. Hướng dẫn này chỉ ra cách phát hiện việc đổi chủ sở hữu, chỉnh sửa lịch sử phiên bản và bất kỳ sửa đổi thuộc tính nào khác giữa hai phiên bản của cùng một tệp, sau đó xuất kết quả ra CSV hoặc JSON để báo cáo tiếp theo.

Tôi gặp vấn đề này khi kiểm toán một bộ 1.200 hợp đồng cho một vụ sáp nhập năm 2024; cuộc kiểm tra metadata đã phát hiện 87 thay đổi sở hữu bất ngờ mà nếu không có tự động hoá sẽ không thể nhìn thấy.

Trong các phần sau, bạn sẽ học cách:

  • Trích xuất toàn bộ metadata từ hai tài liệu.
  • Xác định các thuộc tính được thêm, bị xóa và đã thay đổi.
  • Tập trung vào các thẻ liên quan đến sở hữu và phiên bản.
  • Chuẩn hóa diff thành CSV hoặc JSON cho các chuỗi kiểm toán.

Tại sao So sánh Metadata lại Quan trọng

So sánh metadata chính xác là cần thiết cho:

  • Legal e‑discovery: Chứng minh nguồn gốc tài liệu và phát hiện gian lận.
  • Regulatory compliance: Theo dõi ai đã tạo hoặc sửa đổi tệp cho các cuộc kiểm toán GDPR hoặc SOX.
  • Forensic investigations: Phát hiện các phiên bản ẩn hoặc chỉnh sửa không được phép.

Theo tài liệu GroupDocs.Metadata (2024), hơn 30 % các thất bại trong tuân thủ bắt nguồn từ các thay đổi thuộc tính không được ghi lại.

Yêu cầu trước

  • .NET 6.0 trở lên
  • GroupDocs.Metadata for .NET 24.10+ (temporary license)
  • Hai phiên bản tài liệu (ví dụ: contract_v1.docxcontract_v2.docx)

Cài đặt qua NuGet:

dotnet add package GroupDocs.Metadata

Làm sao để so sánh metadata giữa hai phiên bản tài liệu?

Trả lời: Tải mỗi tệp bằng MetadataFacade, trích xuất tất cả các thuộc tính vào các dictionary, sau đó lặp qua để xây dựng một MetadataDiff phân loại các mục được thêm, bị xóa và đã thay đổi. Diff có thể được kiểm tra bằng mã hoặc ghi ra CSV/JSON cho báo cáo kiểm toán.

Diff Toàn bộ Thuộc tính

Đoạn mã dưới đây minh họa thuật toán diff cốt lõi. Nó sử dụng helper ExtractAllMetadata.Run (xem phần sau) để lấy mọi thuộc tính từ mỗi phiên bản.

// 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;

Các điểm chính:

  • MetadataDiff chứa ba dictionary: Added, Removed, Changed.
  • Thuật toán chạy trong thời gian O(n), phù hợp với các tệp có hàng nghìn thuộc tính.
  • Không thực hiện I/O; người gọi quyết định cách lưu kết quả.

Phát hiện Thay đổi Sở hữu

Các thẻ liên quan đến sở hữu (Author, LastSavedBy, Manager, Company) thường là quan trọng nhất đối với các cuộc kiểm toán pháp lý. Phương thức dưới đây tách riêng các thẻ này và báo cáo bất kỳ sự khác biệt nào.

// 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;

Các điểm chính:

  • Sử dụng các predicate Tags.Person.*Tags.Corporate.Company.
  • Trả về một dictionary, mỗi mục hiển thị cũ → mới.
  • Thích hợp để tạo báo cáo thay đổi sở hữu ngắn gọn.

Trợ giúp: 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;

Phát hiện Thay đổi Lịch sử Phiên bản

Metadata liên quan đến phiên bản (RevisionNumber, TotalEditingTime, LastPrinted) cho biết tài liệu đã được chỉnh sửa bao nhiêu lần. Đoạn mã dưới đây trích xuất các thuộc tính này và làm nổi bật bất kỳ sự chênh lệch nào.

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;

Các điểm chính:

  • Ghi lại cả dấu thời gian và các bộ đếm phiên bản dạng số.
  • Hữu ích để phát hiện các chỉnh sửa ẩn mà không được lưu dưới dạng phiên bản riêng.

Trợ giúp: 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;

Xuất Diff ra CSV

Các nhóm tuân thủ thường cần báo cáo dạng bảng tính. Phương thức sau ghi MetadataDiff vào tệp CSV với bốn cột.

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());

Các điểm chính:

  • CsvEscape bảo đảm các trường có dấu phẩy hoặc ngắt dòng được bao quanh bằng dấu ngoặc kép.
  • Tệp kết quả có thể mở trực tiếp trong Excel hoặc tải lên hệ thống SIEM.

Trợ giúp: CsvEscape

if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
    return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;

Xuất Diff ra JSON

Đối với các pipeline lập trình, payload JSON thường được ưa chuộng hơn. Phương thức dưới đây tạo ra một schema ổn định với ba đối tượng cấp cao.

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());

Các điểm chính:

  • WriteMap ghi các đối tượng key‑value đơn giản cho các phần added/removed.
  • Escape đảm bảo các chuỗi tương thích với JSON.

Trợ giúp: 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;

So sánh Các Phương pháp: Khi Nào Nên Dùng

Phương pháp Thích hợp cho Ưu điểm chính Hạn chế
Full‑Property Diff Kiểm toán pháp y tổng quát Phát hiện mọi thuộc tính được thêm/bị xóa/đã thay đổi Có thể tạo diff lớn đối với tệp phức tạp
Ownership Detection Kiểm tra nguồn gốc pháp lý Chỉ tập trung vào các thẻ mang thông tin nhận dạng Bỏ qua các thay đổi metadata khác
Revision History Detection Phân tích hoạt động chỉnh sửa Nổi bật các bộ đếm thời gian và phiên bản Yêu cầu các thuộc tính này phải tồn tại trong tệp
CSV Export Báo cáo dựa trên bảng tính Dễ mở trong Excel, đọc được bởi con người Giới hạn cấu trúc phẳng
JSON Export Pipeline tự động, dashboard Cấu trúc, máy đọc được Kích thước payload hơi lớn hơn

Chọn Full‑Property Diff khi cần kiểm toán toàn diện; kết hợp với CSV Export để nhanh chóng cung cấp cho các bên liên quan. Đối với các kiểm tra tuân thủ tự động, hãy truyền trực tiếp đầu ra JSON vào hệ thống giám sát của bạn.

Các Thực hành Tốt và Mẹo

  • Giải phóng MetadataFacade ngay khi xong: bọc trong khối using để giải phóng tài nguyên gốc.
  • Giới hạn việc trích xuất chỉ các thẻ cần thiết: lọc bằng Tags.Person.* hoặc Tags.Time.* giúp giảm tiêu thụ bộ nhớ cho các PDF lớn.
  • Xác thực định dạng tệp: metadata.FileFormat == FileFormat.Unknown cho biết tệp không được hỗ trợ hoặc bị hỏng.
  • Phiên bản hoá báo cáo kiểm toán: bao gồm phiên bản thư viện (GroupDocs.Metadata 24.10) trong phần header của tệp xuất để truy xuất nguồn.
  • Bảo mật: không ghi log giá trị thô có thể chứa dữ liệu cá nhân; hãy ẩn PII trước khi lưu.
  • Hiệu năng: với các tài liệu >50 MB, cân nhắc streaming việc trích xuất metadata (hiện chưa được cung cấp trong API) hoặc xử lý các tệp theo batch song song.

Kết luận

GroupDocs.Metadata cung cấp một cách mạnh mẽ, lập trình được để so sánh mọi thông tin nhúng giữa hai phiên bản tài liệu. Bằng cách trích xuất metadata đầy đủ, tập trung vào các thẻ sở hữu hoặc lịch sử phiên bản, và xuất diff ra CSV hoặc JSON, bạn có thể xây dựng quy trình pháp y lặp lại đáp ứng yêu cầu pháp lý, tuân thủ và bảo mật.

Bước tiếp theo:

Tài nguyên Bổ sung