💡 Pełny działający przykład dostępny na GitHubie:
metadata-diff-between-docs-using-groupdocs-metadata-dotnet
Wprowadzenie
Gdy umowa zmienia właściciela w trakcie fuzji, zespół prawny musi udowodnić, że informacje o własności dokumentu – autor, ostatni edytor, nazwa firmy – pozostały niezmienione. Ręczna inspekcja właściwości każdego pliku jest żmudna i podatna na błędy; jednorazowe przeoczenie zmiany może unieważnić audyt zgodności. GroupDocs.Metadata jest biblioteką .NET, która wyodrębnia i porównuje wszystkie osadzone właściwości dokumentu, umożliwiając automatyczną analizę forensic zmian wersja‑do‑wersji. Ten tutorial pokazuje, jak wykrywać zamiany właścicieli, edycje historii wersji oraz inne modyfikacje właściwości pomiędzy dwoma wersjami tego samego pliku, a następnie eksportować wyniki do CSV lub JSON w celu dalszego raportowania.
Zauważyłem ten problem, audytując zestaw 1 200 umów w ramach fuzji w 2024 r.; audyt metadanych ujawnił 87 nieoczekiwanych zmian własności, które byłyby niewidoczne bez automatyzacji.
W kolejnych sekcjach dowiesz się, jak:
- Wyodrębnić wszystkie metadane z dwóch dokumentów.
- Zidentyfikować dodane, usunięte i zmienione właściwości.
- Skupić się na tagach związanych z własnością i historią wersji.
- Serializować różnicę do CSV lub JSON dla ścieżek audytowych.
Why Metadata Comparison Matters
Accurate metadata comparison is essential for:
- Legal e‑discovery: Prove document provenance and detect tampering.
- Regulatory compliance: Track who created or modified a file for GDPR or SOX audits.
- Forensic investigations: Spot hidden revisions or unauthorized edits.
According to the GroupDocs.Metadata documentation (2024), over 30 % of compliance failures stem from undocumented property changes.
Prerequisites
- .NET 6.0 lub nowszy
- GroupDocs.Metadata for .NET 24.10+ (temporary license)
- Dwie wersje dokumentu (np.
contract_v1.docxicontract_v2.docx)
Instalacja przez NuGet:
dotnet add package GroupDocs.Metadata
Jak porównać metadane między dwiema wersjami dokumentu?
Odpowiedź: Załaduj każdy plik przy użyciu MetadataFacade, wyodrębnij wszystkie właściwości do słowników, a następnie iteruj, aby zbudować MetadataDiff, który kategoryzuje dodane, usunięte i zmienione wpisy. Różnicę można przeglądać programowo lub zapisać do CSV/JSON w celu raportowania audytowego.
Pełna różnica właściwości
Poniższy fragment kodu demonstruje podstawowy algorytm różnicowania. Wykorzystuje pomocniczy ExtractAllMetadata.Run (zobacz dalej), aby pobrać każdą właściwość z obu wersji.
// 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;
Kluczowe punkty:
MetadataDiffzawiera trzy słowniki:Added,Removed,Changed.- Algorytm działa w czasie O(n), co jest odpowiednie dla plików z tysiącami właściwości.
- Nie wykonuje operacji I/O; to wywołujący decyduje, jak zapisać wynik.
Wykrywanie zmian własności
Tagi związane z własnością (Author, LastSavedBy, Manager, Company) są najczęściej najważniejsze w audytach prawnych. Poniższa metoda izoluje te tagi i raportuje wszelkie różnice.
// 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;
Kluczowe punkty:
- Używa predykatów
Tags.Person.*orazTags.Corporate.Company. - Zwraca słownik, w którym każdy wpis pokazuje stare → nowe wartości.
- Idealny do generowania zwięzłego raportu o zmianach własności.
Pomocnicza metoda: 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;
Wykrywanie zmian w historii wersji
Metadane związane z wersjami (RevisionNumber, TotalEditingTime, LastPrinted) ujawniają, ile razy dokument był edytowany. Poniższy fragment wyodrębnia te właściwości i podkreśla wszelkie odchylenia.
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;
Kluczowe punkty:
- Rejestruje zarówno znaczniki czasu, jak i liczniki wersji.
- Przydatne do wykrywania ukrytych edycji, które nie zostały zapisane jako osobne wersje.
Pomocnicza metoda: 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;
Eksportowanie różnicy do CSV
Zespoły ds. zgodności często potrzebują raportu przyjaznego arkuszom kalkulacyjnym. Poniższa metoda zapisuje MetadataDiff do pliku CSV z czterema kolumnami.
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());
Kluczowe punkty:
CsvEscapebezpiecznie cytuje pola zawierające przecinki lub znaki nowej linii.- Powstały plik można otworzyć bezpośrednio w Excelu lub załadować do systemu SIEM.
Pomocnicza metoda: CsvEscape
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Contains(",") || s.Contains("\"") || s.Contains("\n"))
{
return "\"" + s.Replace("\"", "\"\"") + "\"";
}
return s;
Eksportowanie różnicy do JSON
Dla pipeline’ów programistycznych częściej preferowany jest format JSON. Poniższa metoda generuje stabilny schemat z trzema obiektami najwyższego poziomu.
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());
Kluczowe punkty:
WriteMapzapisuje proste obiekty klucz‑wartość dla sekcji added/removed.Escapezapewnia ciągi zgodne z JSON‑em.
Pomocnicze metody: 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;
Porównanie metod: Kiedy używać której
| Metoda | Najlepsze zastosowanie | Główne zalety | Ograniczenia |
|---|---|---|---|
| Full‑Property Diff | Ogólne audyty forensic | Wykrywa każde dodane/usunięte/zmienione pole | Może generować duży diff przy skomplikowanych plikach |
| Ownership Detection | Kontrole pochodzenia prawnego | Skupia się wyłącznie na tagach tożsamościowych | Ignoruje inne zmiany metadanych |
| Revision History Detection | Analiza aktywności edycyjnej | Podkreśla liczniki czasu i wersji | Wymaga obecności odpowiednich właściwości w pliku |
| CSV Export | Raportowanie w arkuszach | Łatwy do otwarcia w Excelu, czytelny dla człowieka | Ograniczony do płaskiej struktury |
| JSON Export | Zautomatyzowane pipeline’y, dashboardy | Strukturalny, czytelny dla maszyn | Nieco większy rozmiar payloadu |
Wybierz pełną różnicę właściwości, gdy potrzebny jest kompleksowy audyt; połącz ją z eksportem CSV, aby szybko przedstawić wyniki interesariuszom. Dla automatycznych kontroli zgodności, przekieruj wyjście JSON bezpośrednio do systemu monitorującego.
Najlepsze praktyki i wskazówki
- Szybko zwalniaj
MetadataFacade: otaczaj go blokiemusing, aby zwolnić zasoby natywne. - Ogranicz ekstrakcję do potrzebnych tagów: filtrowanie po
Tags.Person.*lubTags.Time.*zmniejsza zużycie pamięci w dużych PDF‑ach. - Waliduj format pliku:
metadata.FileFormat == FileFormat.Unknownwskazuje na nieobsługiwany lub uszkodzony plik. - Wersjonuj raporty audytowe: umieść wersję biblioteki (
GroupDocs.Metadata 24.10) w nagłówku wyeksportowanego pliku dla przejrzystości. - Bezpieczeństwo: nigdy nie loguj surowych wartości właściwości, które mogą zawierać dane osobowe; maskuj PII przed ich utrwaleniem.
- Wydajność: dla dokumentów >50 MB rozważ strumieniowe wyodrębnianie metadanych (obecnie nieudostępnione w API) lub przetwarzaj pliki w równoległych partiach.
Zakończenie
GroupDocs.Metadata zapewnia solidny, programowy sposób porównywania każdego elementu osadzonej informacji pomiędzy dwoma wersjami dokumentu. Poprzez wyodrębnienie pełnych metadanych, skupienie się na tagach własności lub historii wersji oraz eksport różnicy do CSV lub JSON, możesz zbudować powtarzalne workflow forensic, które spełniają wymogi prawne, regulacyjne i bezpieczeństwa.
Kolejne kroki:
- Zapoznaj się z pełną listą obsługiwanych tagów, aby dopasować diff do konkretnych wymagań regulacyjnych.
- Dowiedz się, jak porównywać wiele dokumentów jednocześnie (API reference).
- Przejrzyj dodatkowe projekty przykładowe na GitHubie pod kątem scenariuszy przetwarzania wsadowego (Examples Repo).