💡 مثال کامل قابل اجرا در GitHub موجود است:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

مقدمه

هنگامی که یک قرارداد در طول ادغام به دست دیگری می‌رسد، تیم حقوقی باید ثابت کند که اطلاعات مالکیت سند – نویسنده، آخرین ویرایشگر، نام شرکت – دست نخورده باقی مانده است. بررسی دستی ویژگی‌های هر فایل زمان‌بر و مستعد خطا است؛ یک تغییر از دست رفته می‌تواند ممیزی انطباق را نامعتبر کند. GroupDocs.Metadata یک کتابخانه .NET است که هر ویژگی توکار یک سند را استخراج و مقایسه می‌کند و امکان تحلیل قانونی خودکار تغییرات نسخه‑به‑نسخه را فراهم می‌آورد. این آموزش نشان می‌دهد چگونه تعویض مالکیت، ویرایش‌های تاریخچه بازنگری و هر تغییر دیگری در ویژگی‌ها بین دو نسخه یک فایل را شناسایی کرده و نتایج را به CSV یا JSON برای گزارش‌گیری بعدی صادر کنید.

من این مشکل را هنگام ممیزی ۱٬۲۰۰ قرارداد برای یک ادغام در سال ۲۰۲۴ مواجه شدم؛ ممیزی متادیتا ۸۷ تغییر مالکیت غیرمنتظره را نشان داد که بدون خودکارسازی قابل مشاهده نبودند.

در بخش‌های زیر خواهید آموخت که چگونه:

  • تمام متادیتا را از دو سند استخراج کنید.
  • ویژگی‌های اضافه‌شده، حذف‌شده و تغییر یافته را شناسایی کنید.
  • بر برچسب‌های مربوط به مالکیت و بازنگری تمرکز کنید.
  • اختلاف را به CSV یا JSON برای ردپای ممیزی تبدیل کنید.

چرا مقایسه متادیتا مهم است

مقایسه دقیق متادیتا برای موارد زیر ضروری است:

  • کشف قانونی (e‑discovery): اثبات منبع سند و شناسایی دستکاری.
  • انطباق مقرراتی: ردیابی اینکه چه کسی فایلی را ایجاد یا اصلاح کرده برای ممیزی‌های GDPR یا SOX.
  • تحقیقات قانونی: کشف بازنگری‌های مخفی یا ویرایش‌های غیرمجاز.

بر اساس مستندات GroupDocs.Metadata (۲۰۲۴)، بیش از ۳۰ ٪ از شکست‌های انطباق ناشی از تغییرات غیرمستند ویژگی‌ها است.

پیش‌نیازها

  • .NET 6.0 یا بالاتر
  • GroupDocs.Metadata برای .NET 24.10+ (مجوز موقت)
  • دو نسخه سند (مثلاً contract_v1.docx و contract_v2.docx)

نصب از طریق NuGet:

dotnet add package GroupDocs.Metadata

چگونه متادیتا را بین دو نسخه سند مقایسه کنم؟

پاسخ: هر فایل را با 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 استفاده می‌کند.
  • یک دیکشنری برمی‌گرداند که هر ورودی قدیم → جدید را نشان می‌دهد.
  • برای تولید گزارش مختصر تغییرات مالکیت ایده‌آل است.

کمکی: 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;

نکات کلیدی:

  • هم زمان‌سنج‌ها و شمارنده‌های عددی بازنگری را ضبط می‌کند.
  • برای کشف ویرایش‌های مخفی که به‌عنوان نسخه‌های جداگانه ذخیره نشده‌اند مفید است.

کمکی: 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 بارگذاری شود.

کمکی: CsvEscape

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

خروجی‌گیری اختلاف به JSON

برای خطوط لوله برنامه‌نویسی، معمولاً یک payload 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 اشیاء کلید‑مقدار ساده برای بخش‌های added/removed می‌نویسد.
  • Escape رشته‌ها را برای JSON سازگار می‌کند.

کمکی: 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;

مقایسه روش‌ها: چه زمانی از هر کدام استفاده کنیم

روش بهترین استفاده برای مزایای کلیدی محدودیت‌ها
اختلاف تمام ویژگی‌ها ممیزی‌های قانونی عمومی تشخیص هر ویژگی اضافه‌شده/حذف‌شده/تغییر یافته ممکن است برای فایل‌های پیچیده حجم زیادی از اختلاف تولید کند
شناسایی مالکیت بررسی منبع قانونی فقط بر برچسب‌های حاوی هویت تمرکز می‌کند تغییرات دیگر متادیتا را نادیده می‌گیرد
شناسایی تاریخچه بازنگری تحلیل فعالیت‌های ویرایشی زمان‌بندی و شمارنده‌های بازنگری را برجسته می‌کند نیاز به وجود ویژگی‌ها در فایل دارد
خروجی CSV گزارش‌گیری مبتنی بر صفحه‌گسترده آسان برای باز کردن در اکسل، قابل خواندن برای انسان محدود به ساختار صاف
خروجی JSON خطوط لوله خودکار، داشبوردها ساختارمند، قابل خواندن توسط ماشین بار کمی بزرگتر

زمانی که به یک ممیزی جامع نیاز دارید، اختلاف تمام ویژگی‌ها را انتخاب کنید؛ برای مرور سریع توسط ذینفعان، خروجی CSV را ترکیب کنید. برای بررسی‌های خودکار انطباق، خروجی JSON را مستقیماً به سیستم مانیتورینگ خود بفرستید.

بهترین شیوه‌ها و نکات

  • MetadataFacade را به‌سرعت آزاد کنید: آن را در یک بلوک using بپیچید تا منابع بومی آزاد شوند.
  • استخراج را به برچسب‌های مورد نیاز محدود کنید: فیلتر کردن با Tags.Person.* یا Tags.Time.* مصرف حافظه را برای PDFهای بزرگ کاهش می‌دهد.
  • قالب فایل را اعتبارسنجی کنید: metadata.FileFormat == FileFormat.Unknown نشان‌دهنده فایل پشتیبانی‌نشده یا خراب است.
  • گزارش‌های خود را نسخه‌بندی کنید: نسخه کتابخانه (GroupDocs.Metadata 24.10) را در هدر فایل خروجی برای قابلیت ردیابی بنویسید.
  • امنیت: هرگز مقادیر خام ویژگی‌ها که ممکن است شامل داده‌های شخصی باشند را لاگ نکنید؛ قبل از ذخیره‌سازی، PII را ماسک کنید.
  • عملکرد: برای اسناد بزرگ‌تر از ۵۰ MB، استخراج متادیتا را به صورت استریم (که در API فعلاً در دسترس نیست) در نظر بگیرید یا فایل‌ها را به صورت دسته‌های موازی پردازش کنید.

نتیجه‌گیری

GroupDocs.Metadata یک روش برنامه‌ای قدرتمند برای مقایسه هر قطعه اطلاعات توکار بین دو نسخه سند فراهم می‌کند. با استخراج کامل متادیتا، تمرکز بر برچسب‌های مالکیت یا بازنگری، و خروجی‌گیری اختلاف به CSV یا JSON، می‌توانید جریان‌های کاری قانونی، انطباقی و امنیتی قابل تکرار بسازید که نیازهای قانونی، انطباقی و امنیتی را برآورده می‌سازند.

مراحل بعدی:

  • فهرست کامل برچسب‌های پشتیبانی‌شده را در فهرست کامل برچسب‌ها بررسی کنید تا اختلاف خود را برای نیازهای نظارتی خاص تنظیم کنید.
  • یاد بگیرید چگونه چندین سند را به‌صورت همزمان مقایسه کنید (مرجع API).
  • پروژه‌های نمونه اضافی در GitHub را برای سناریوهای پردازش دسته‌ای ببینید (مخزن نمونه‌ها).

منابع اضافی