💡 Полный рабочий пример доступен на GitHub:
metadata-diff-doc-versions-using-groupdocs-metadata-nodejs
Introduction
Сравнение метаданных — это возможность GroupDocs.Metadata, позволяющая выявлять скрытые изменения между версиями документов, что дает аудиторам возможность быстро проверять подлинность. Когда юридическим командам необходимо доказать, что контракт не был подделан, невидимые свойства — автор, редактор, временные метки правок — часто рассказывают истинную историю.
Я столкнулся с этой проблемой, проверяя пакет соглашений с поставщиками; каждый файл указывал одну и ту же дату создания, но скрытые поля автора различались, и ручной аудит занял бы часы.
Why hidden metadata matters for legal teams
Юридические и комплаенс‑специалисты полагаются на неизменяемые доказательства. Даже если видимый текст остаётся тем же, изменение поля Creator или LastPrinted может свидетельствовать о несанкционированных правках. Выявление таких сдвигов на ранних этапах предотвращает дорогостоящие споры и удовлетворяет требования регуляторных аудитов.
Our solution with GroupDocs.Metadata
GroupDocs.Metadata для Node.js предоставляет единый API, который извлекает каждое поддерживаемое свойство, сравнивает две ревизии и формирует структурированные отчёты о различиях. SDK поддерживает DOCX, PDF, XLSX и многие другие форматы «из коробки», поэтому нет необходимости писать собственные парсеры для каждого типа файлов.
Step 1 – Pulling all metadata from a document
Ниже‑приведённый помощник открывает файл, проходит по всему дереву свойств и возвращает обычный JavaScript‑объект вида имя свойства → значение. Он использует AnySpecification для захвата как встроенных, так и пользовательских полей.
/** Extract all metadata from a file */
function extractAllMetadata(documentPath) {
const result = {};
const metadata = new groupdocs.Metadata(documentPath);
try {
const properties = metadata.findProperties(new groupdocs.AnySpecification());
for (let i = 0; i < properties.getCount(); i++) {
const p = properties.get_Item(i);
const name = p.getName();
const value = valueToString(p);
result[name] = value;
}
} finally {
metadata.close();
}
return result;
}
Performance note: extracting metadata from a 5 MB DOCX completes in under 200 ms on a typical 2 GHz CPU [Docs][DOCS_URL].
Step 2 – Spotting differences between two revisions
Имея обе версии в виде простых объектов, процедура сравнения формирует три группы — added, removed и changed — и предоставляет удобный геттер totalChanges.
/** Compare two metadata maps */
function compareMetadataSets(pathV1, pathV2) {
const v1 = extractAllMetadata(pathV1);
const v2 = extractAllMetadata(pathV2);
const added = {};
const removed = {};
const changed = {};
for (const k of Object.keys(v2)) {
if (!(k in v1)) added[k] = v2[k];
else if (v1[k] !== v2[k]) changed[k] = { from: v1[k], to: v2[k] };
}
for (const k of Object.keys(v1)) {
if (!(k in v2)) removed[k] = v1[k];
}
return {
added,
removed,
changed,
get totalChanges() {
return Object.keys(this.added).length + Object.keys(this.removed).length + Object.keys(this.changed).length;
},
};
}
Step 3 – Ownership and revision insights (optional)
Detecting ownership changes
Юридические аудиторы часто сосредотачиваются на том, кто создал или отредактировал документ. Этот помощник изолирует соответствующие поля и помечает любые отклонения.
/** Ownership change detection */
function detectOwnershipChanges(pathV1, pathV2) {
const v1 = readOwnership(pathV1);
const v2 = readOwnership(pathV2);
const all = new Set([...Object.keys(v1), ...Object.keys(v2)]);
const changes = {};
for (const k of all) {
const oldV = v1[k] !== undefined ? v1[k] : '<missing>';
const newV = v2[k] !== undefined ? v2[k] : '<missing>';
if (oldV !== newV) changes[k] = { from: oldV, to: newV };
}
return changes;
}
Detecting revision‑history changes
Номер ревизии, общее время редактирования и временные метки последней печати раскрывают скрытую активность.
/** Revision history change detection */
function detectRevisionHistory(pathV1, pathV2) {
const v1 = readRevision(pathV1);
const v2 = readRevision(pathV2);
const all = new Set([...Object.keys(v1), ...Object.keys(v2)]);
const changes = {};
for (const k of all) {
const oldV = v1[k] !== undefined ? v1[k] : '<missing>';
const newV = v2[k] !== undefined ? v2[k] : '<missing>';
if (oldV !== newV) changes[k] = { from: oldV, to: newV };
}
return changes;
}
Step 4 – Exporting audit reports
CSV export
Формат CSV удобен для Excel или импорта в SIEM. Поля экранируются, чтобы корректно обрабатывать запятые и переносы строк.
/** Export diff to CSV */
function exportDiffToCsv(diff, outputPath) {
const rows = ['change_type,property,old_value,new_value'];
for (const [k, v] of Object.entries(diff.added)) rows.push(`added,${esc(k)},,${esc(v)}`);
for (const [k, v] of Object.entries(diff.removed)) rows.push(`removed,${esc(k)},${esc(v)},`);
for (const [k, o] of Object.entries(diff.changed)) rows.push(`changed,${esc(k)},${esc(o.from)},${esc(o.to)}`);
fs.writeFileSync(outputPath, rows.join('\n') + '\n', 'utf-8');
}
JSON export
JSON сохраняет иерархическую структуру для программного потребления.
/** Export diff to JSON */
function exportDiffToJson(diff, outputPath) {
const payload = { added: diff.added, removed: diff.removed, changed: diff.changed };
fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2), 'utf-8');
}
Full driver script
Скрипт связывает все части, выводит краткое резюме и записывает отчёты в CSV и JSON.
const path = require('path');
const fs = require('fs');
const groupdocs = require('@groupdocs/groupdocs.metadata');
function esc(txt) { return /[",\n]/.test(txt) ? `"${txt.replace(/"/g, '""')}"` : txt; }
const diff = compareMetadataSets('resources/document-v1.docx', 'resources/document-v2.docx');
console.log(`Total metadata changes: ${diff.totalChanges}`);
exportDiffToCsv(diff, path.resolve('metadata-diff.csv'));
exportDiffToJson(diff, path.resolve('metadata-diff.json'));
Запуск node index.js создаёт metadata-diff.csv и metadata-diff.json в корне проекта.
How can I compare metadata between two versions of a document?
Вы сравниваете метаданные, загружая каждую ревизию с помощью класса Metadata, извлекая плоскую карту «имя → значение» через extractAllMetadata и передавая две карты в