💡 مثال كامل يعمل متاح على GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet
المقدمة
عند انتقال العقد بين الأطراف أثناء الاندماج، يجب على الفريق القانوني إثبات أن معلومات ملكية المستند – المؤلف، آخر محرر، اسم الشركة – ظلت سليمة. الفحص اليدوي لخصائص كل ملف مرهق وعرضة للأخطاء؛ أي تغيير مفقود يمكن أن يبطل تدقيق الامتثال. GroupDocs.Metadata هي مكتبة .NET تستخرج وتقارن كل خاصية مدمجة في المستند، مما يتيح تحليلًا جنائيًا آليًا للتغييرات بين الإصدارات. يوضح هذا البرنامج التعليمي كيفية اكتشاف تبديل الملكية، تعديلات تاريخ المراجعات، وأي تعديل آخر للخصائص بين نسختين من نفس الملف، ثم تصدير النتائج إلى CSV أو JSON لتقارير لاحقة.
واجهت هذه المشكلة أثناء تدقيق مجموعة من 1,200 عقد لاندماج عام 2024؛ كشف تدقيق البيانات الوصفية عن 87 تغييرًا غير متوقع في الملكية كان سيبقى غير مرئي بدون الأتمتة.
في الأقسام التالية ستتعلم كيفية:
- استخراج جميع البيانات الوصفية من مستندين.
- تحديد الخصائص المضافة، المحذوفة، والمعدلة.
- التركيز على العلامات المتعلقة بالملكية وتاريخ المراجعات.
- تسلسل الفرق إلى CSV أو JSON لسلاسل التدقيق.
لماذا مقارنة البيانات الوصفية مهمة
المقارنة الدقيقة للبيانات الوصفية أساسية لـ:
- الاكتشاف القانوني الإلكتروني: إثبات أصل المستند واكتشاف التلاعب.
- الامتثال التنظيمي: تتبع من أنشأ أو عدل ملفًا لتدقيقات GDPR أو SOX.
- التحقيقات الجنائية: اكتشاف المراجعات المخفية أو التعديلات غير المصرح بها.
وفقًا لتوثيق 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
كيف أقارن البيانات الوصفية بين نسختين من المستند؟
الإجابة: حمّل كل ملف باستخدام 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
لأنابيب البرامج، غالبًا ما يُفضَّل تنسيق 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.
المساعد: 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;
مقارنة الطرق: متى تستخدم كل منها
| الطريقة | الأنسب لـ | المزايا الرئيسية | القيود |
|---|---|---|---|
| Full‑Property Diff | تدقيقات جنائية عامة | يكتشف أي خاصية مضافة/محذوفة/معدلة | قد ينتج فرقًا كبيرًا للملفات المعقدة |
| Ownership Detection | فحوصات أصلية قانونية | يركز فقط على العلامات التي تحمل هوية | يتجاهل تغييرات البيانات الوصفية الأخرى |
| Revision History Detection | تحليل نشاط التحرير | يبرز العدادات الزمنية والرقمية للمراجعات | يتطلب وجود الخصائص في الملف |
| CSV Export | تقارير تعتمد على الجداول | سهل الفتح في Excel، قابل للقراءة البشرية | يقتصر على بنية مسطحة |
| JSON Export | أنابيب آلية، لوحات معلومات | منظم، قابل للقراءة آليًا | حجم الحمولة أكبر قليلًا |
اختر فرق الخصائص الكامل عندما تحتاج إلى تدقيق شامل؛ اجمعه مع تصدير CSV للمراجعات السريعة من قبل أصحاب المصلحة. للتدقيق الآلي، مرر مخرجات JSON مباشرة إلى نظام المراقبة الخاص بك.
أفضل الممارسات والنصائح
- تخلص من
MetadataFacadeبسرعة: ضعها داخل كتلةusingلتحرير الموارد الأصلية. - قصر الاستخراج على العلامات المطلوبة: الترشيح باستخدام
Tags.Person.*أوTags.Time.*يقلل استهلاك الذاكرة للملفات الكبيرة مثل PDF. - تحقق من صيغ الملفات:
metadata.FileFormat == FileFormat.Unknownيدل على ملف غير مدعوم أو تالف. - أدر نسخ تقارير التدقيق: أدرج نسخة المكتبة (
GroupDocs.Metadata 24.10) في رأس الملف المصدر لتتبع الأصالة. - الأمان: لا تسجل القيم الخام للخصائص التي قد تحتوي على بيانات شخصية؛ قم بإخفاء الـ PII قبل التخزين.
- الأداء: للملفات التي يزيد حجمها عن 50 MB، فكر في استخراج البيانات الوصفية بشكل متدفق (حالياً غير متاح في الـ API) أو عالج الملفات على دفعات متوازية.
الخلاصة
توفر GroupDocs.Metadata طريقة برمجية قوية لمقارنة كل قطعة من المعلومات المدمجة بين نسختين من المستند. من خلال استخراج البيانات الوصفية بالكامل، التركيز على علامات الملكية أو المراجعة، وتصدير الفرق إلى CSV أو JSON، يمكنك بناء سير عمل جنائي قابل للتكرار يلبي المتطلبات القانونية، والامتثال، والأمنية.
الخطوات التالية:
- استكشف القائمة الكاملة للعلامات المدعومة لتخصيص الفرق وفقًا لاحتياجات تنظيمية محددة.
- تعلّم كيفية مقارنة عدة مستندات في آنٍ واحد (API reference).
- اطلع على مشاريع عينات إضافية على GitHub لسيناريوهات المعالجة الدفعية (Examples Repo).