💡 דוגמה מלאה עובדת זמינה ב‑GitHub:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet

מבוא

כאשר חוזה עובר בעלות במהלך מיזוג, צוות המשפטי חייב להוכיח שמידע הבעלות של המסמך – מחבר, העורך האחרון, שם החברה – נשאר שלם. בדיקה ידנית של תכונות הקובץ היא משימה מייגעת ושגויה; שינוי אחד שלא נצפה יכול לבטל ביקורת ציות. GroupDocs.Metadata היא ספריית .NET שמחלצת ומשווה כל תכונה משובצת של מסמך, ומאפשרת ניתוח פורנזי אוטומטי של שינויים מגרסה לגרסה. מדריך זה מראה כיצד לאתר החלפות בעלות, עריכות היסטוריית גרסאות, וכל שינוי תכונה אחר בין שני גרסאות של אותו קובץ, ולאחר מכן לייצא את הממצאים ל‑CSV או JSON לדיווח נוסף.

נתקלתי בבעיה זו בעת בדיקת 1,200 חוזים למיזוג בשנת 2024; בדיקת המטא‑דטה חשפה 87 שינויי בעלות בלתי צפויים שהיו נסתרים ללא אוטומציה.

בקטעים הבאים תלמדו כיצד:

  • לחלץ את כל המטא‑דטה משני מסמכים.
  • לזהות תכונות שנוספו, הוסרו או השתנו.
  • להתמקד בתגיות בעלות ובתגיות ספציפיות להיסטוריית גרסאות.
  • לסריאליזציה של ההבדלים ל‑CSV או JSON לצורכי מסלולי ביקורת.

למה השוואת מטא‑דטה חשובה

השוואת מטא‑דטה מדויקת חיונית עבור:

  • גילוי משפטי (e‑discovery): הוכחת מקוריות המסמך וגילוי זיופים.
  • ציות רגולטורי: מעקב אחרי מי יצר או שינה קובץ עבור ביקורות GDPR או SOX.
  • חקירות פורנזיות: איתור גרסאות מוסתרות או עריכות בלתי מורשות.

לפי תיעוד GroupDocs.Metadata (2024), יותר מ‑30 % מכשלונות הציות נובעים משינויים בתכונות שלא תועדו.

דרישות מוקדמות

  • .NET 6.0 או גרסה מאוחרת יותר
  • GroupDocs.Metadata for .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), מתאים לקבצים עם אלפי תכונות.
  • אין קריאות קלט/פלט; הקריאה מחליטה כיצד לשמור את התוצאה.

זיהוי שינויי בעלות

תגיות הקשורות לבעלות (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;

נקודות מפתח:

  • משתמש ב‑predicates 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;

השוואת שיטות: מתי להשתמש בכל אחת

שיטה מתאים ל‑ יתרונות מרכזיים מגבלות
Full‑Property Diff ביקורות פורנזיות כלליות מזהה כל תכונה שנוספה/הוסרה/השתנתה עשוי לייצר diff גדול עבור קבצים מורכבים
Ownership Detection בדיקות מקוריות משפטיות מתמקד רק בתגיות נושאות זהות מתעלם משינויים במטא‑דטה אחרים
Revision History Detection ניתוח פעילות עריכה מדגיש חותמות זמן ומונים של גרסאות דורש שהתכונות יהיו קיימות בקובץ
CSV Export דיווח מבוסס גיליון פתיחה קלה ב‑Excel, קריא לבני אדם מוגבל למבנה שטוח
JSON Export צינורות אוטומטיים, לוחות מחוונים מובנה, קריא למכונה משקל payload מעט גדול יותר

בחרו ב‑Full‑Property Diff כאשר נדרשת ביקורת מקיפה; שלבו עם ייצוא 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, ניתן לבנות זרימות עבודה פורנזיות חוזרות שמספקות דרישות משפטיות, ציות וביטחון.

צעדים הבאים:

משאבים נוספים