💡 Pełny działający przykład dostępny na GitHubie:
document-version-metadata-diff-python

Co zbudujesz

W tym przewodniku porównasz (diff) wszystkie właściwości metadanych pomiędzy dwiema wersjami dokumentu i wydrukujesz dokładnie to, co zostało dodane, usunięte lub zmienione. Diff wersji metadanych to porównanie na poziomie właściwości dwóch rewizji jednego pliku, które wychwytuje sygnały, których porównanie tekstu nigdy nie zobaczy: nowy Twórca, podniesiony RevisionNumber, sesja edycji zalogowana po zamknięciu przeglądu. Po zakończeniu będziesz mieć działające rozwiązanie plus dwa wyspecjalizowane detektory i dwa formaty eksportu, wszystkie pobrane z uruchamialnego repozytorium zawierającego przykładową parę rewizji.

Poziom umiejętności: średniozaawansowany programista Python
Co potrzebujesz: Python 3, pip oraz dwie rewizje jednego dokumentu

Mój pierwszy uruchomiony skrypt wykrył zmianę wartości Company, której nikt w zespole nie pamiętał, że wprowadził; ta jedna linijka opłaciła całą konfigurację. Wszystko poniżej jest gotowe do skopiowania i wklejenia oraz mieści się w setce linii.

Pipeline jest celowo nudny: dwa otwarcia pliku, trzy comprehensions słowników, pętla drukująca. Nuda jest celem. Spory o wersje rozstrzygane są na podstawie tego, czy metodę da się wyjaśnić i powtórzyć, a tak mały skrypt może być w całości przeczytany przez każdego, kto kwestionuje wynik.


1. Instalacja

pip install groupdocs-metadata-net==26.5

Repozytorium towarzyszące przypina tę wersję i dostarcza document-v1.docx oraz document-v2.docx, więc poniższy kod działa „as‑is”. Przypnij wersję, z którą przeprowadzałeś audyt; powtarzalność jest częścią dowodu.


2. Główny kod

Wczytaj oba drzewa właściwości, a następnie sklasyfikuj delta przy użyciu logiki zbiorów. To cały diff:

# Flatten a file's complete property tree into a dict
def read_props(path):
    props = {}
    with Metadata(path) as metadata:
        for p in metadata.find_properties(lambda p: p.name is not None):
            props[p.name] = (str(p.interpreted_value) if p.interpreted_value is not None
                             else (str(p.value) if p.value is not None else ""))
    return props

v1 = read_props("resources/document-v1.docx")
v2 = read_props("resources/document-v2.docx")

# Classify every key; changed entries keep both values
added = {k: v for k, v in v2.items() if k not in v1}
removed = {k: v for k, v in v1.items() if k not in v2}
changed = {k: (v1[k], v2[k]) for k in v1 if k in v2 and v1[k] != v2[k]}

print(f"added={len(added)} removed={len(removed)} changed={len(changed)}")
for k, (old_v, new_v) in changed.items():
    print(f"  {k}: {old_v} -> {new_v}")

To minimum, którego potrzebujesz. Oczekuj małych liczb przy prawdziwych parach rewizji; delta w dziesiątkach zazwyczaj oznacza, że plik przeszedł zmianę szablonu lub migrację przechowywania po drodze. Kolejne sekcje wyjaśniają kluczowe wywołania i pokazują najczęstsze pierwsze modyfikacje.


3. Jak to działa

  • Metadata: menedżer kontekstu, który otwiera plik i zwalnia go przy wyjściu; po jednej instancji na rewizję.
  • find_properties: przeszukuje wbudowane pola, własne właściwości i XMP w jednym przebiegu, zwracając wszystko, co spełnia predykat.
  • interpreted_value: czytelna dla człowieka forma właściwości; używanie jej oznacza, że daty i wyliczenia porównywane są jako łańcuchy, które możesz wydrukować w raporcie.
  • Nazwy kwalifikowane jako klucze: wbudowane i własne pola nie mogą kolidować w słowniku, więc logika zbiorów pozostaje bezpieczna.

Nic tutaj nie parsuje struktury DOCX. Dokumentacja produktu wymienia ponad 170 formatów obsługiwanych tym samym wywołaniem, więc identyczny skrypt diffuje pary PDF lub XLSX.

Jedną z cech projektu, którą warto wymienić, jest to, że granica API kończy się na dwóch wywołaniach read_props. Wszystko po nich to standardowa biblioteka Pythona, więc testy jednostkowe, progi i reguły alertów nigdy nie dotykają warstwy dokumentu. Zespoły, które opakowują to w usługę, zazwyczaj cache’ują wyekstrahowane słowniki per rewizja i pozwalają każdemu dalszemu sprawdzeniu ich używać, utrzymując IO pliku na jednym otwarciu na wersję, niezależnie od liczby zadawanych pytań.


4. Typowe modyfikacje

Wykrywanie wyłącznie zmian własności

Gdy pytanie brzmi „kto dotknął ten plik”, filtruj już przy odczycie przy pomocy predykatów tagów zamiast filtrować cały diff później:

# Identity fields only, whatever the format calls them
def read_ownership(path):
    result = {}
    with Metadata(path) as metadata:
        props = metadata.find_properties(lambda p:
            Tags.person.creator in list(p.tags)
            or Tags.person.editor in list(p.tags)
            or Tags.person.manager in list(p.tags)
            or Tags.corporate.company in list(p.tags))
        for prop in props:
            result[prop.name] = (str(prop.interpreted_value)
                                 if prop.interpreted_value is not None
                                 else (str(prop.value) if prop.value is not None else ""))
    return result

Uruchom tę samą pętlę delta na dwóch takich słownikach, używając <missing> jako wartości domyślnej, aby pole, które zniknęło, nadal się pojawiło. Nazwy predykatów nie określają pola, co pozwala jednemu detektorowi obsługiwać każdy format, który biblioteka odczytuje.

Śledzenie osi czasu edycji

Zamień predykat na Tags.time plus reguły nazw liczników, a detektor zgłosi zmiany w RevisionNumber, TotalEditingTime i LastPrinted:

props = metadata.find_properties(lambda p:
    Tags.time.modified in list(p.tags)
    or Tags.time.created in list(p.tags)
    or Tags.time.printed in list(p.tags)
    or (p.name is not None and ("Revision" in p.name
        or "EditTime" in p.name or "EditingTime" in p.name)))

Eksport raportu audytowego

Wyniki, które pozostają w konsoli, giną tam. Cztery kolumny pokrywają arkusz kalkulacyjny i przypadek SIEM:

with open("output/diff.csv", "w", encoding="utf-8", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["change_type", "property", "old_value", "new_value"])
    for k, v in added.items():
        writer.writerow(["added", k, "", v])
    for k, v in removed.items():
        writer.writerow(["removed", k, v, ""])
    for k, (old_v, new_v) in changed.items():
        writer.writerow(["changed", k, old_v, new_v])

Repozytorium zawiera także eksporter JSON ze stabilnym schematem trzech map, przeznaczonym dla pulpitów i API zarządzania przypadkami.


Gdzie to działa w praktyce

Trzy wdrożenia pojawiają się najczęściej. Pipeline przyjmujące porównują każdy przychodzący dokument z kopią już zarejestrowaną i kwarantannują pary z zmianami własności. Zadania zgodności uruchamiają diff według harmonogramu i archiwizują CSV per para, budując oś czasu właściwości, której nikt nie musi odtwarzać później. Narzędzia do rozstrzygania sporów uruchamiają oba detektory na żądanie, ponieważ gdy pojawia się roszczenie, pierwsze pytanie zawsze brzmi „kto i kiedy dotknął pliku”, a nie „co zmieniło się w paragrafie czwartym”.

Czwarty wzorzec, diffowanie pliku względem własnego ostatniego znanego dobrego migawki, ponownie używa tego samego kodu z przechowywanym słownikiem po jednej stronie. We wszystkich przypadkach plik eksportu jest dostarczalnym wynikiem; wyjście konsoli to jedynie szum postępu. Wzorzec kodu wyjścia (exit‑code) podąża za main.py w repozytorium, więc planery i CI traktują nieudany assert jako nieudane uruchomienie bez dodatkowej konfiguracji. Żaden z nich nie wymagał kodu poza tym, co pokazano na tej stronie.


Co liczy się jako zmiana warta flagi?

Wszystko, co klasyfikuje diff, plus kontekst, który dodasz. Dodane i usunięte właściwości zawsze warto sprawdzić, ponieważ oznaczają zmianę struktury, a nie tylko wartości. W przypadku zmienionych wpisów większość zespołów najpierw alarmuje o grupach własności i rewizji, a resztę traktuje jako informacyjną. Detektory istnieją po to, by pierwszy przebieg kosztował tylko jedno wywołanie funkcji.


5. Szybkie odniesienie: kluczowe wywołania

Wywołanie Co robi
Metadata(path) Otwiera plik; menedżer kontekstu zajmuje się zwolnieniem
find_properties(predicate) Zwraca każdą właściwość spełniającą predykat, we wszystkich warstwach
p.interpreted_value Czytelna wartość; w razie braku spada do p.value
Tags.person.* / Tags.corporate.company Klasyfikacja tożsamości, niezależna od formatu
Tags.time.* Klasyfikacja znaczników czasu dla detektora rewizji

Zobacz pełną dokumentację API dla pełnego zakresu wyszukiwania i tagowania. Słownik tagów jest większy niż te wiersze; grupy tagów origin, content i legal używają tego samego testu przynależności.


6. Typowe problemy i ich rozwiązania

Diff jest ogromny i wygląda jak szum
→ Prawdopodobnie podane ścieżki nie są rewizjami tego samego dokumentu. Napraw: zweryfikuj pochodzenie przed diffowaniem; niepowiązane pliki generują bezsensowne delty.

Znane pole autora nie pojawia się w detektorze własności
→ Niektórzy producenci przechowują tożsamość w nieotagowanych własnych polach. Napraw: uruchom pełny diff raz, znajdź rzeczywistą nazwę pola i rozszerz predykat o regułę nazwy.

Konsola wyświetla ostrzeżenie trybu ewaluacji
→ Nie znaleziono pliku licencji. Napraw: wskaż LICENSE_PATH w main.py na swój plik .lic, albo pozostaw tryb ewaluacji w fazie rozwoju; logika jest identyczna.

Daty wyświetlają się jako surowe liczby seryjne
→ Surowe p.value wślizgnęło się gdzieś do czytnika. Napraw: zachowaj wzorzec najpierw interpreted_value w read_props; to powód, dla którego raporty pozostają czytelne.


Co dalej?

Masz działający diff metadanych. Oto kilka kierunków rozwoju:

  • Batchowanie: pętla skryptu po parach dokumentów i zapisywanie CSV per para; koszt per para to dwa otwarcia pliku, a CSV‑y ładnie się łączą w widok biblioteczny.
  • Harmonogram: main.py w repozytorium asercjonuje każdy krok i zwraca właściwy kod wyjścia, co wpasowuje się bezpośrednio w CI lub scheduler.
  • Przejdź przez wersję tutorialu: przewodnik po przypadkach użycia buduje tę samą pipeline w trzech stopniowanych tutorialach.
  • Zobacz cały projekt: document-version-metadata-diff-python z wstępnie przygotowaną parą rewizji.

Zasoby