💡 Volledig werkend voorbeeld beschikbaar op GitHub:
document-version-metadata-diff-python
Wat je gaat bouwen
In deze gids vergelijk je elke metadataproperty tussen twee versies van een document en druk je precies af wat is toegevoegd, verwijderd of gewijzigd. Een metadata‑versiediff is een vergelijking op property‑niveau van twee revisies van één bestand, en het vangt signalen die een tekstvergelijking nooit ziet: een nieuwe Creator, een verhoogd RevisionNumber, een bewerkingssessie die is gelogd nadat de review is gesloten. Aan het einde heb je een werkende oplossing plus twee gerichte detectors en twee exportformaten, allemaal afkomstig uit een uitvoerbare repository met een voorbeeld‑revisie‑paar.
Vaardigheidsniveau: intermediaire Python‑ontwikkelaar
Wat je nodig hebt: Python 3, pip, en twee revisies van één document
Mijn eerste uitvoering van dit script gaf een wijziging in de Company‑waarde aan die niemand in het team zich herinnerde te hebben gemaakt; die ene regel betaalde de installatie. Alles hieronder is klaar om te kopiëren‑plakken en telt ruim onder de honderd regels.
De pipeline is opzettelijk saai: twee bestandsopeningen, drie dict‑comprehensions, een print‑lus. Saai is het punt. Versiedisputen worden beslist op basis van of de methode kan worden uitgelegd en herhaald, en een script zo klein kan volledig worden gelezen door iedereen die de bevinding betwist.
1. Installeren
pip install groupdocs-metadata-net==26.5
De companion repository pinnt deze versie en levert document-v1.docx en document-v2.docx zodat de onderstaande code direct werkt. Pin de versie waarmee jouw audit is uitgevoerd; reproduceerbaarheid maakt deel uit van het bewijs.
2. De kerncode
Lees beide property‑bomen en classificeer vervolgens de delta met set‑logica. Dit is de volledige 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}")
Dat is het minimum dat je nodig hebt. Verwacht kleine aantallen bij echte revisie‑paren; een delta van tientallen betekent meestal dat het bestand een sjabloon‑wijziging of een opslag‑migratie heeft ondergaan. De volgende secties leggen de belangrijkste aanroepen uit en tonen de aanpassingen die de meeste teams eerst toevoegen.
3. Hoe het werkt
Metadata: de context‑manager die een bestand opent en bij het verlaten vrijgeeft; één instantie per revisie.find_properties: doorloopt ingebouwde velden, aangepaste properties en XMP in één stap, en retourneert alles wat het predicaat accepteert.interpreted_value: de mens‑leesbare vorm van een property; door dit te verkiezen worden datums en enumeraties vergeleken als strings die je in een rapport kunt afdrukken.- Gekwalificeerde namen als sleutels: ingebouwde en aangepaste velden kunnen niet botsen in de dict, zodat de set‑logica veilig blijft.
Niets hiervan parseert DOCX‑structuren. De productdocumentatie vermeldt meer dan 170 formaten achter dezelfde aanroep, dus hetzelfde script vergelijkt PDF‑ of XLSX‑paren.
Een ander kenmerk van het ontwerp dat het vermelden waard is: de API‑grens eindigt bij de twee read_props‑aanroepen. Alles daarna is standaard‑bibliotheek‑Python, zodat unit‑tests, drempels en waarschuwingsregels nooit de documentlaag aanraken. Teams die dit in een service wikkelen, cachen meestal de geëxtraheerde dicts per revisie en laten elke downstream‑check ze hergebruiken, waardoor er per versie slechts één bestand‑open wordt uitgevoerd, ongeacht hoeveel vragen er worden gesteld.
4. Veelvoorkomende aanpassingen
Alleen eigendom‑wijzigingen detecteren
Wanneer de vraag is “wie heeft dit bestand aangeraakt”, filter dan tijdens het lezen met tag‑predicaten in plaats van de volledige diff achteraf te filteren:
# 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
Voer dezelfde delta‑lus uit over twee van deze dicts, gebruik <missing> als standaard zodat een veld dat verdween toch naar voren komt. De predicaat‑namen bevatten geen veld, wat het mogelijk maakt dat één detector elke door de bibliotheek gelezen indeling bedient.
De bewerkings‑tijdlijn bijhouden
Vervang het predicaat door Tags.time plus teller‑naamregels en de detector rapporteert verplaatsingen van RevisionNumber, TotalEditingTime en 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)))
Een audit‑rapport exporteren
Resultaten die alleen in de console blijven, verdwijnen daar. Vier kolommen dekken het spreadsheet‑ en SIEM‑geval:
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])
De repository bevat ook een JSON‑exporteur met een stabiel drie‑map‑schema voor dashboards en case‑management‑API’s.
Waar dit in de praktijk draait
Drie implementaties komen steeds terug. Inname‑pipelines vergelijken elk binnenkomend document met de kopie die al in het register staat en plaatsen paren met identiteitswijzigingen in quarantaine. Compliance‑taken voeren de diff volgens een schema uit en archiveren de CSV per paar, waardoor een property‑tijdlijn ontstaat die later niet meer hoeft te worden gereconstrueerd. En dispute‑tools draaien beide detectors op aanvraag, want wanneer een claim binnenkomt is de eerste vraag altijd wie het bestand heeft aangeraakt en wanneer, niet wat er in alinea vier is veranderd.
Een vierde patroon, een bestand vergelijken met zijn eigen laatst‑bekende‑goede snapshot, hergebruikt dezelfde code met een opgeslagen dict aan één kant. In al deze gevallen is het exportbestand het eindproduct; console‑output is alleen voortgangs‑ruis. Het exit‑code‑patroon van het script volgt de main.py van de repository, zodat planners en CI een mislukte assert behandelen als een mislukte run zonder extra bedrading. Geen van hen had extra code nodig boven wat deze pagina toont.
Wat telt als een wijziging die gemarkeerd moet worden?
Alles wat de diff classificeert plus de context die jij toevoegt. Toegevoegde en verwijderde properties zijn altijd het bekijken waard omdat ze betekenen dat de structuur is veranderd in plaats van alleen een waarde. Voor gewijzigde entries alarmeren de meeste teams eerst op de identiteits‑ en revisiegroepen en behandelen de rest als informatief. De detectors bestaan zodat de eerste pass slechts één functie‑aanroep kost.
5. Snelle referentie: Belangrijke aanroepen
| Aanroep | Wat het doet |
|---|---|
Metadata(path) |
Opent het bestand; context‑manager regelt vrijgave |
find_properties(predicate) |
Retourneert elke property die het predicaat accepteert, over alle lagen heen |
p.interpreted_value |
Mens‑leesbare waarde; valt terug op p.value |
Tags.person.* / Tags.corporate.company |
Identiteitsclassificatie, formaat‑onafhankelijk |
Tags.time.* |
Tijdstempelclassificatie voor de revisiedetector |
Zie de complete API‑referentie voor de volledige zoek‑ en tag‑surface. De tag‑vocabulaire is groter dan deze rijen; origin, content en legal‑tag‑groepen volgen dezelfde lidmaatschapstest.
6. Veelvoorkomende problemen & oplossingen
De diff is enorm en leest als ruis
→ De twee paden zijn waarschijnlijk geen revisies van één document. Oplossing: valideer de herkomst vóór het diffen; niet‑gerelateerde bestanden leveren betekenisloze deltas op.
Een bekend auteur‑veld verschijnt nooit in de eigendom‑detector
→ Sommige producenten slaan identiteit op in niet‑getagde aangepaste velden. Oplossing: voer de volledige diff één keer uit, vind de echte veldnaam, en breid het predicaat uit met een naam‑regel.
Console toont een evaluation‑mode waarschuwing
→ Er is geen licentiebestand gevonden. Oplossing: wijs LICENSE_PATH in main.py naar jouw .lic‑bestand, of houd evaluation‑mode voor ontwikkeling; de logica is identiek.
Datums worden afgedrukt als ruwe seriële getallen
→ De ruwe p.value is ergens in een reader terechtgekomen. Oplossing: behoud het interpreted_value‑eerste patroon uit read_props; dit is de reden dat rapporten leesbaar blijven.
Wat is het volgende?
Je hebt een werkende metadata‑diff. Hier kun je verder mee:
- Batchen: laat het script over documentparen lopen en sla de CSV per paar op; de kosten per paar zijn twee bestandsopeningen, en de CSV’s kunnen netjes worden samengevoegd voor een bibliotheek‑breed overzicht.
- Inplannen: de
main.pyvan de repository assert elke stap en retourneert een juiste exit‑code, die direct in CI of een planner kan worden opgenomen. - De tutorial‑versie doorlopen: de use case guide bouwt dezelfde pipeline in drie graduele tutorials.
- Het volledige project bekijken: document-version-metadata-diff-python met het meegeleverde revisie‑paar.