💡 Exemplo completo em funcionamento disponível no GitHub:
document-version-metadata-diff-python
O que você vai construir
Neste guia você comparará (diff) cada propriedade de metadados entre duas versões de um documento e imprimirá exatamente o que foi adicionado, removido ou alterado. Um diff de versão de metadados é uma comparação nível‑propriedade de duas revisões de um mesmo arquivo, e captura sinais que uma comparação de texto nunca vê: um novo Creator, um RevisionNumber incrementado, uma sessão de edição registrada após a revisão ter sido encerrada. Ao final, você terá uma solução funcional mais dois detectores focados e dois formatos de exportação, tudo extraído de um repositório executável que contém um par de revisões de exemplo.
Nível de habilidade: desenvolvedor Python intermediário
O que você precisa: Python 3, pip e duas revisões de um mesmo documento
Na minha primeira execução deste script, ele sinalizou uma mudança no valor da propriedade Company que ninguém da equipe lembrava ter feito; aquela única linha pagou a configuração. Tudo abaixo está pronto para copiar‑colar e tem menos de cem linhas no total.
O pipeline foi propositalmente simplório: duas aberturas de arquivo, três compreensões de dicionário, um laço de impressão. A simplicidade é o objetivo. Disputas de versão são decididas com base em um método que pode ser explicado e repetido, e um script tão pequeno pode ser lido integralmente por quem questionar o resultado.
1. Instalar
pip install groupdocs-metadata-net==26.5
O repositório complementar fixa essa versão e inclui document-v1.docx e document-v2.docx, de modo que o código abaixo funciona exatamente como está. Fixe a versão que sua auditoria utilizou; a reprodutibilidade faz parte da evidência.
The Core Code
Leia ambas as árvores de propriedades e, em seguida, classifique o delta com lógica de conjuntos. Este é o 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}")
Isso é o mínimo necessário. Espere contagens baixas em pares de revisão genuínos; um delta na casa das dezenas geralmente indica que o arquivo passou por uma mudança de modelo ou por uma migração de armazenamento no caminho. As próximas seções explicam as chamadas principais e mostram as personalizações que a maioria das equipes adiciona primeiro.
3. Como funciona
Metadata: o gerenciador de contexto que abre um arquivo e o libera ao sair; uma instância por revisão.find_properties: percorre campos internos, propriedades personalizadas e XMP em uma única passagem, retornando tudo que o predicado aceita.interpreted_value: a forma legível por humanos de uma propriedade; priorizá‑la faz com que datas e enumerações sejam comparadas como strings que podem ser impressas em um relatório.- Nomes qualificados como chaves: campos internos e personalizados não colidem no dicionário, portanto a lógica de conjuntos permanece segura.
Nada aqui analisa a estrutura interna de DOCX. A documentação do produto lista mais de 170 formatos suportados por essa mesma chamada, de modo que o script idêntico funciona para pares de PDF ou XLSX.
Um outro aspecto do design vale ser mencionado: o limite da API termina nas duas chamadas read_props. Tudo depois disso é Python da biblioteca padrão, de modo que testes unitários, limiares e regras de alerta nunca tocam a camada de documento. Equipes que encapsulam isso em um serviço geralmente armazenam em cache os dicionários extraídos por revisão e permitem que verificações subsequentes os reutilizem, mantendo I/O de arquivo em uma única abertura por versão, independentemente de quantas perguntas sejam feitas.
4. Personalizações comuns
Detectar apenas mudanças de propriedade de titularidade
Quando a pergunta é “quem tocou este arquivo”, filtre na leitura usando predicados de tags em vez de filtrar o diff completo depois:
# 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
Execute o mesmo laço de diff sobre dois desses dicionários, usando <missing> como valor padrão para que um campo que desapareceu ainda apareça. Os nomes dos predicados não referenciam nenhum campo específico, o que permite que um detector sirva todos os formatos que a biblioteca lê.
Rastrear a linha do tempo de edição
Troque o predicado por Tags.time mais regras de nomes de contadores e o detector reportará movimentos de RevisionNumber, TotalEditingTime e 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 um relatório de auditoria
Descobertas que permanecem apenas no console morrem ali. Quatro colunas cobrem a planilha e o caso de 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])
O repositório também inclui um exportador JSON com um esquema estável de três mapas para dashboards e APIs de gerenciamento de casos.
Onde isso roda na prática
Três implantações aparecem com frequência. Pipelines de ingestão comparam cada documento que chega contra a cópia já registrada e isolam pares com mudanças de identidade. Jobs de conformidade executam o diff em agenda e arquivam o CSV por par, construindo uma linha do tempo de propriedades que ninguém precisa reconstruir depois. Ferramentas de disputa rodam ambos os detectores sob demanda, porque quando surge uma alegação a primeira pergunta sempre é quem tocou o arquivo e quando, não o que mudou no parágrafo quatro.
Um quarto padrão, comparar um arquivo contra seu próprio snapshot último conhecido como bom, reutiliza o mesmo código com um dicionário armazenado de um lado. Em todos eles, o arquivo de exportação é o entregável; a saída do console é apenas ruído de progresso. O padrão de código de saída segue o main.py do repositório, de modo que agendadores e CI tratam uma asserção falha como execução falha sem necessidade de wiring extra. Nenhum deles precisou de código além do que esta página mostra.
O que conta como mudança que vale a pena sinalizar?
Qualquer coisa que o diff classifique, mais o contexto que você acrescentar. Propriedades adicionadas e removidas sempre merecem atenção porque indicam que a estrutura mudou, não apenas um valor. Para entradas alteradas, a maioria das equipes alerta primeiro nos grupos de identidade e revisão e trata o restante como informativo. Os detectores existem para que a primeira passagem custe apenas uma chamada de função.
5. Referência rápida: chamadas principais
| Chamada | O que faz |
|---|---|
Metadata(path) |
Abre o arquivo; o gerenciador de contexto cuida da liberação |
find_properties(predicate) |
Retorna todas as propriedades que o predicado aceita, em todas as camadas |
p.interpreted_value |
Valor legível por humanos; recai para p.value |
Tags.person.* / Tags.corporate.company |
Classificação de identidade, independente de formato |
Tags.time.* |
Classificação de timestamp para o detector de revisão |
Consulte a referência completa da API para a pesquisa completa e a superfície de tagging. O vocabulário de tags é maior que essas linhas; grupos de origem, conteúdo e legais seguem o mesmo teste de pertencimento.
6. Problemas comuns e correções
O diff é enorme e parece ruído
→ Provavelmente os dois caminhos não são revisões do mesmo documento. Corrija: valide a procedência antes de comparar; arquivos não relacionados produzem deltas sem sentido.
Um campo de autor conhecido nunca aparece no detector de titularidade
→ Alguns produtores armazenam identidade em campos personalizados sem tags. Corrija: execute o diff completo uma vez, encontre o nome real do campo e amplie o predicado com uma regra de nome.
O console mostra um aviso de modo de avaliação
→ Nenhum arquivo de licença foi encontrado. Corrija: aponte LICENSE_PATH em main.py para seu arquivo .lic, ou mantenha o modo de avaliação para desenvolvimento; a lógica é idêntica.
Datas são impressas como números seriais brutos
→ O p.value bruto escapou para um leitor em algum ponto. Corrija: mantenha o padrão interpreted_value‑primeiro do read_props; ele é a razão de os relatórios permanecerem legíveis.
O que vem a seguir?
Você tem um diff de metadados funcional. Aqui estão os próximos passos:
- Processar em lote: faça um loop do script sobre pares de documentos e armazene o CSV por par; o custo por par são duas aberturas de arquivo, e os CSVs podem ser concatenados facilmente para uma visão de biblioteca inteira.
- Agendar: o
main.pydo repositório faz asserções em cada etapa e devolve um código de saída adequado, que se encaixa direto em CI ou em um agendador. - Seguir o tutorial versionado: o guia de caso de uso constrói o mesmo pipeline em três tutoriais graduados.
- Ver o projeto completo: document-version-metadata-diff-python com o par de revisões de exemplo.