💡 Full working example available on GitHub:
document-version-metadata-diff-python
Qué vas a construir
En esta guía compararás (diff) cada propiedad de metadatos entre dos versiones de un documento y mostrarás exactamente qué se añadió, eliminó o cambió. Un diff de versiones de metadatos es una comparación a nivel de propiedad de dos revisiones de un mismo archivo, y captura señales que una comparación de texto nunca ve: un nuevo Creator, un RevisionNumber incrementado, una sesión de edición registrada después de que la revisión se cerró. Al final tendrás una solución funcional más dos detectores enfocados y dos formatos de exportación, todo extraído de un repositorio ejecutable que incluye un par de revisiones de muestra.
Nivel de habilidad: desarrollador Python intermedio
Lo que necesitas: Python 3, pip y dos revisiones de un mismo documento
Mi primera ejecución de este script detectó un cambio en el valor de Company que nadie del equipo recordaba haber hecho; esa única línea justificó la configuración. Todo lo que sigue está listo para copiar‑pegar y tiene menos de cien líneas en total.
La canalización es deliberadamente aburrida: dos aperturas de archivo, tres comprensiones de diccionario, un bucle de impresión. El aburrimiento es el objetivo. Las disputas de versiones se deciden según si el método puede explicarse y repetirse, y un script tan pequeño puede leerse completo por quien cuestione el hallazgo.
1. Instalación
pip install groupdocs-metadata-net==26.5
El repositorio complementario fija esta versión y entrega document-v1.docx y document-v2.docx, de modo que el código a continuación funciona tal cual. Fija la versión con la que se ejecutó tu auditoría; la reproducibilidad forma parte de la evidencia.
2. El código principal
Lee ambos árboles de propiedades y luego clasifica el delta con lógica de conjuntos. Este es el diff completo:
# 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}")
Eso es lo mínimo que necesitas. Espera recuentos bajos en pares de revisión genuinos; un delta de decenas suele indicar que el archivo pasó por un cambio de plantilla o una migración de almacenamiento en el camino. Las siguientes secciones explican las llamadas clave y muestran las personalizaciones que la mayoría de los equipos añaden primero.
3. Cómo funciona
Metadata: el gestor de contexto que abre un archivo y lo libera al salir; una instancia por revisión.find_properties: recorre campos incorporados, propiedades personalizadas y XMP en una sola pasada, devolviendo todo lo que acepte el predicado.interpreted_value: la forma legible por humanos de una propiedad; preferirla hace que fechas y enumeraciones se comparen como cadenas que puedes imprimir en un informe.- Nombres calificados como claves: los campos incorporados y personalizados no pueden colisionar en el diccionario, por lo que la lógica de conjuntos se mantiene segura.
Nada aquí analiza estructuras DOCX. La documentación del producto enumera más de 170 formatos bajo la misma llamada, de modo que el mismo script difiere pares PDF o XLSX.
Otro aspecto del diseño que vale la pena mencionar: el límite de la API termina en las dos llamadas read_props. Todo lo que sigue es Python de biblioteca estándar, por lo que pruebas unitarias, umbrales y reglas de alerta nunca tocan la capa del documento. Los equipos que envuelven esto en un servicio suelen almacenar en caché los diccionarios extraídos por revisión y permiten que cada verificación descendente los reutilice, manteniendo la E/S de archivo en una apertura por versión sin importar cuántas preguntas se formulen.
4. Personalizaciones comunes
Detectar solo cambios de titularidad
Cuando la pregunta es “¿quién tocó este archivo?”, filtra en el momento de la lectura con predicados de etiquetas en lugar de filtrar el diff completo después:
# 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
Ejecuta el mismo bucle de delta sobre dos de estos diccionarios, usando <missing> como valor predeterminado para que un campo que desaparezca siga apareciendo. Los nombres de los predicados no incluyen campo alguno, lo que permite que un detector sirva a cualquier formato que la biblioteca lea.
Rastrear la línea de tiempo de edición
Cambia el predicado por Tags.time más reglas de nombres de contadores y el detector informa movimientos de RevisionNumber, TotalEditingTime y 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)))
Exportar un informe de auditoría
Los hallazgos que permanecen en la consola mueren allí. Cuatro columnas cubren la hoja de cálculo y el caso 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])
El repositorio también incluye un exportador JSON con un esquema estable de tres mapas para paneles y APIs de gestión de casos.
5. Dónde se ejecuta en la práctica
Tres despliegues aparecen con frecuencia. Las canalizaciones de ingestión difieren cada documento entrante contra la copia ya registrada y ponen en cuarentena los pares con cambios de identidad. Los trabajos de cumplimiento ejecutan el diff según un calendario y archivan el CSV por par, construyendo una línea de tiempo de propiedades que nadie tiene que reconstruir después. Y las herramientas de disputa ejecutan ambos detectores bajo demanda, porque cuando llega una reclamación la pregunta inicial siempre es quién tocó el archivo y cuándo, no qué cambió en el párrafo cuatro.
Un cuarto patrón, difundir un archivo contra su propia instantánea última conocida buena, reutiliza el mismo código con un diccionario almacenado en un lado. En todos ellos, el archivo de exportación es el entregable; la salida de consola es solo ruido de progreso. El patrón de código de salida sigue el main.py del repositorio, de modo que planificadores y CI tratan una aserción fallida como una ejecución fallida sin cableado extra. Ninguno necesitó código más allá de lo que muestra esta página.
6. ¿Qué cuenta como un cambio que vale la pena señalar?
Cualquier cosa que el diff clasifique más el contexto que añadas. Las propiedades añadidas y eliminadas siempre merecen revisión porque indican que la estructura cambió, no solo un valor. Para las entradas modificadas, la mayoría de los equipos alerta primero sobre los grupos de identidad y revisión y trata el resto como informativo. Los detectores existen para que el primer paso cueste una sola llamada a función.
7. Referencia rápida: llamadas clave
| Llamada | Qué hace |
|---|---|
Metadata(path) |
Abre el archivo; el gestor de contexto se encarga de liberarlo |
find_properties(predicate) |
Devuelve cada propiedad que acepte el predicado, a través de todas las capas |
p.interpreted_value |
Valor legible por humanos; recurre a p.value si no está disponible |
Tags.person.* / Tags.corporate.company |
Clasificación de identidad, independiente del formato |
Tags.time.* |
Clasificación de marcas de tiempo para el detector de revisiones |
Consulta la referencia completa de la API para obtener toda la superficie de búsqueda y etiquetado. El vocabulario de etiquetas es más amplio que estas filas; los grupos de origen, contenido y legales siguen la misma prueba de membresía.
8. Problemas comunes y soluciones
El diff es enorme y parece ruido
→ Probablemente las dos rutas no sean revisiones del mismo documento. Solución: valida la procedencia antes de diferir; archivos no relacionados generan deltas sin sentido.
Un campo de autor conocido nunca aparece en el detector de titularidad
→ Algunos productores guardan la identidad en campos personalizados sin etiquetas. Solución: ejecuta el diff completo una vez, encuentra el nombre real del campo y amplía el predicado con una regla de nombre.
La consola muestra una advertencia de modo de evaluación
→ No se encontró archivo de licencia. Solución: apunta LICENSE_PATH en main.py a tu archivo .lic, o mantén el modo de evaluación para desarrollo; la lógica es idéntica.
Las fechas se imprimen como números seriales crudos
→ El p.value sin procesar se coló en algún lector. Solución: conserva el patrón interpreted_value‑primero en read_props; es la razón por la que los informes siguen siendo legibles.
9. ¿Qué sigue?
Tienes un diff de metadatos funcional. Aquí tienes algunas ideas para continuar:
- Procesarlo por lotes: recorre el script sobre pares de documentos y guarda el CSV por par; el costo por par son dos aperturas de archivo, y los CSV se concatenan limpiamente para una vista a nivel de biblioteca.
- Programarlo: el
main.pydel repositorio verifica cada paso y devuelve un código de salida adecuado, que encaja directamente en CI o en un programador. - Seguir el tutorial versionado: la guía de casos de uso construye la misma canalización en tres tutoriales graduados.
- Ver el proyecto completo: document-version-metadata-diff-python con el par de revisiones de muestra.