💡 Повний робочий приклад доступний на GitHub:
document-version-metadata-diff-python
Що ви створите
У цьому посібнику ви порівняєте кожну властивість метаданих між двома версіями документа і виведете точно те, що було додано, видалено або змінено. Порівняння версій метаданих — це порівняння на рівні властивостей двох ревізій одного файлу, яке виявляє сигнали, що текстове порівняння ніколи не побачить: новий Creator, збільшений RevisionNumber, сесія редагування, зареєстрована після закриття перегляду. Після завершення у вас буде працююче рішення плюс два спеціалізованих детектори та два формати експорту, все це взято з готового репозиторію з прикладом пари ревізій.
Рівень навичок: середній Python‑розробник
Що потрібно: Python 3, pip і дві ревізії одного документа
Мій перший запуск цього скрипту виявив зміну значення Company, яку ніхто з команди не пам’ятав, що виправдало налаштування. Усе нижче готове до копіювання та вставки і вміщується в межах сотні рядків.
Конвеєр навмисно простий: два відкриття файлів, три словникових включення, цикл виведення. Простість — це мета. Спори щодо версій вирішуються на підставі того, чи можна метод пояснити і повторити, а скрипт такого розміру можна повністю прочитати тим, хто ставить під сумнів результати.
1. Встановлення
pip install groupdocs-metadata-net==26.5
Супутній репозиторій фіксує цю версію і містить document-v1.docx та document-v2.docx, тому наведений код працює «як є». Зафіксуйте ту версію, з якою виконувалося ваше аудиторське дослідження; відтворюваність — частина доказової бази.
2. Основний код
Прочитайте обидва дерева властивостей, а потім класифікуйте дельту за допомогою логіки множин. Ось весь код порівняння:
# Перетворити повне дерево властивостей файлу у словник
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")
# Класифікуємо кожен ключ; змінені записи зберігають обидва значення
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}")
Це мінімум, який вам потрібен. Очікуйте невеликі підрахунки на реальних парах ревізій; дельта в десятках зазвичай означає, що файл пройшов через зміну шаблону або міграцію сховища. Наступні розділи пояснюють ключові виклики та показують налаштування, які більшість команд додають першими.
3. Як це працює
Metadata: менеджер контексту, який відкриває файл і закриває його при виході; один екземпляр на ревізію.find_properties: проходить вбудовані поля, користувацькі властивості та XMP за один прохід, повертаючи все, що задовольняє предикат.interpreted_value: людсько‑читабельна форма властивості; використання її означає, що дати та перерахування порівнюються як рядки, які можна вивести у звіті.- Qualified names as keys: вбудовані та користувацькі поля не можуть конфліктувати у словнику, тому логіка множин залишається безпечною.
Тут нічого не парситься у структурах DOCX. Документація продукту перелічує понад 170 форматів, які підтримуються одним викликом, тому той самий скрипт працює і з PDF, і з XLSX.
Ще одна особливість дизайну варта згадки: межа API закінчується на двох викликах read_props. Все, що після них — це стандартна бібліотека Python, тому юніт‑тести, пороги та правила сповіщень ніколи не торкаються шару документу. Команди, які обгортають це у сервіс, зазвичай кешують отримані словники на ревізію і дозволяють всім подальшим перевіркам їх повторно використовувати, залишаючи лише одне відкриття файлу на версію, незалежно від кількості запитань.
4. Поширені налаштування
Виявлення лише змін власності
Коли питання звучить «хто торкнувся цього файлу», фільтруйте під час читання за допомогою предикатів тегів, а не після побудови повної дельти:
# Тільки поля ідентифікації, незалежно від назви у форматі
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
Запустіть той самий цикл дельти над двома такими словниками, використовуючи <missing> як значення за замовчуванням, щоб поле, яке зникло, все одно з’явилось у результаті. Імена предикатів не залежать від конкретного поля, що дозволяє одному детектору працювати з будь‑яким форматом, який читає бібліотека.
Відстеження хронології редагування
Замініть предикат на Tags.time плюс правила іменування лічильників, і детектор повідомлятиме про зміни RevisionNumber, TotalEditingTime та 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)))
Експорт аудиторського звіту
Знаходження, які залишаються лише у консолі, втрачаються. Чотири стовпці покривають електронну таблицю та випадок 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])
У репозиторії також є експортер у JSON з стабільною схемою трьох мап для дашбордів та API управління випадками.
5. Де це використовується на практиці
Три типові розгортання. Пайплайни прийому порівнюють кожен надходящий документ із копією, вже збереженою в реєстрі, і ізолюють пари з змінами ідентифікації. Роботи з комплаєнсу запускають дельту за розкладом і архівують CSV‑файл для кожної пари, створюючи хронологію властивостей, яку не доведеться відтворювати пізніше. Інструменти вирішення спорів запускають обидва детектори за запитом, бо коли надходить претензія, перше питання завжди «хто і коли торкнувся файлу», а не «що змінилося у четвертому абзаці».
Четвертий патерн — порівняння файлу з його останнім відомим «хорошим» знімком — повторно використовує той самий код з збереженим словником з однієї сторони. У всіх випадках файл експорту є кінцевим результатом; вивід у консоль — лише шум прогресу. Шаблон коду вихідного коду відповідає main.py у репозиторії, тому планувальники та CI трактують невдалу перевірку як провал запуску без додаткових налаштувань. Жоден з цих сценаріїв не потребував коду, окрім показаного на цій сторінці.
6. Що вважається зміною, яку варто позначити?
Будь‑яка дельта, яку класифікує скрипт, плюс контекст, який ви додаєте. Додані та видалені властивості завжди варто переглянути, бо вони означають зміну структури, а не лише значення. Для змінених записів більшість команд спочатку сповіщають про групи ідентифікації та ревізії, а решту розглядають як інформаційні. Детектори існують, щоб перший прохід коштував лише одного виклику функції.
7. Швидка довідка: ключові виклики
| Виклик | Що робить |
|---|---|
Metadata(path) |
Відкриває файл; менеджер контексту забезпечує закриття |
find_properties(predicate) |
Повертає кожну властивість, яку задовольняє предикат, по всіх рівнях |
p.interpreted_value |
Людсько‑читабельне значення; у випадку відсутності — p.value |
Tags.person.* / Tags.corporate.company |
Класифікація ідентифікації, незалежна від формату |
Tags.time.* |
Класифікація часових міток для детектора ревізій |
Дивіться повну довідку API для повного переліку пошуку та тегування. Словник тегів ширший, ніж ці рядки; групи тегів origin, content та legal працюють за тим же принципом тесту членства.
8. Поширені проблеми та виправлення
Дельта надзвичайно велика і виглядає шумом
→ Шляхи, ймовірно, не є ревізіями одного документа. Виправлення: перевірте походження файлів перед порівнянням; несумісні файли дають беззмістовні дельти.
Поле відомого автора не з’являється у детекторі власності
→ Деякі продуценти зберігають ідентифікацію у нетегованих користувацьких полях. Виправлення: один раз запустіть повний дельт, знайдіть реальну назву поля і розширте предикат правилом за назвою.
У консолі з’являється попередження про режим оцінки
→ Не знайдено файл ліцензії. Виправлення: вкажіть LICENSE_PATH у main.py до вашого .lic файлу, або залиште режим оцінки для розробки; логіка ідентично працює.
Дати виводяться як сирі серійні числа
→ Десь у читачі потрапив p.value. Виправлення: залишайте шаблон interpreted_value‑first у read_props; саме він забезпечує читабельність звітів.
9. Що далі?
У вас є працююче порівняння метаданих. Ось куди можна рухатися далі:
- Пакетування: обійдіть скрипт по парах документів і зберігайте CSV для кожної пари; вартість — два відкриття файлів, а CSV легко конкатенувати для огляду всієї бібліотеки.
- Планування:
main.pyу репозиторії робить перевірки і повертає правильний код виходу, що підходить для CI або планувальника. - Пройти навчальний варіант: посібник по використанню будує той самий конвеєр у трьох поступово ускладнюваних уроках.
- Переглянути весь проєкт: document-version-metadata-diff-python з підготовленою парою ревізій.