💡 Полный рабочий пример доступен на GitHub:
office-metadata-pii-cleanup-nodejs
Introduction
Точка загрузки принимает DOCX от сотрудника и сохраняет его в тикете клиента. Текст в порядке. Свойства — нет: в файле указаны имя автора, последний сохранявший, менеджер отдела из корпоративного шаблона и, поскольку файл пришёл из SharePoint, утверждающий, который его одобрил.
Санитайзер метаданных — небольшая программа, удаляющая эти свойства перед сохранением файла, а затем проверяющая свою работу. В этом руководстве мы создаём такой скрипт на Node.js с помощью GroupDocs.Metadata за четыре шага: выбираем свойства по тегу, выбираем их по имени, стираем всё, когда выборка перестаёт помогать, и проверяем, что осталось. Каждый шаг состоит из нескольких строк, а готовый скрипт занимает менее ста строк.
Why Metadata Sanitization Matters
Данные накапливаются без какого‑либо выбора. Word записывает Author и LastSavedBy из учётной записи ОС при каждом сохранении, ведёт счётчик ревизий, отслеживает TotalEditingTime и фиксирует LastPrinted. Серверы документов добавляют пути рабочего процесса, идентификаторы утверждающих и URI типов контента при проверке‑в‑хранилище. Ни одна из этих сведений не отображается при чтении или печати документа, поэтому проверяющий никогда их не замечает.
Смысл делать это в Node.js, а не вручную, в том, что скрипт возвращает числа: каждый вызов удаления сообщает, сколько свойств было удалено, и это количество можно записать в журнал, проверить в тесте или привязать к записи, к которой относится документ.
Есть и вторая причина, менее очевидная, пока не запущен пакетный процесс. Ручная очистка — решение, принимаемое один раз для каждого файла тем, кто в данный момент с ним работает, поэтому два человека, очищающие один и тот же тип документа, получат разные результаты. Скрипт фиксирует правило в одном месте: те же четыре тега, те же списки подстрок, применяемые одинаково независимо от того, обрабатывает очередь три файла или три тысячи.
Prerequisites
Пакет работает в Node.js через Java, поэтому машине нужен Java‑runtime рядом с Node.
Installation
npm install @groupdocs/groupdocs.metadata
Пример проекта фиксирует версию 26.7 и добавляет запись overrides, задающую nan версии ^2.22.0, что позволяет собрать нативный биндинг под текущие релизы Node. Без файла лицензии библиотека работает в режиме оценки, чего достаточно для выполнения всех шагов здесь.
Step 1 - Select properties by what they mean
Имена свойств различаются между форматами и пакетами, поэтому первое правило опирается на теги. ContainsTagSpecification принимает тег и сопоставляет любое свойство, содержащие его; .or() объединяет спецификации в одну.
const T = groupdocs.Tags;
const spec = new groupdocs.ContainsTagSpecification(T.getPerson().getCreator())
.or(new groupdocs.ContainsTagSpecification(T.getPerson().getEditor()))
.or(new groupdocs.ContainsTagSpecification(T.getPerson().getManager()))
.or(new groupdocs.ContainsTagSpecification(T.getCorporate().getCompany()));
const affected = metadata.removeProperties(spec);
metadata.save(outputPath);
Key points:
- Четыре тега покрывают группу идентификации: создатель, редактор, менеджер и корпоративное поле компании.
- Title, Subject и Keywords остаются нетронутыми, поэтому индекс записей, использующий их, продолжает работать.
removePropertiesвозвращает количество затронутых свойств, а не логическое значение.
Оборачивайте всё в try/finally с вызовом metadata.close() в блоке finally. Привязка держит файл открытым до этого момента, а цикл без закрытия быстро исчерпывает дескрипторы.
Step 2 - Select properties by name
Комментарии, счётчики ревизий и серверные поля не несут тегов. Для них WithNameSpecification(needle, false) сопоставляет любое свойство, имя которого содержит подстроку needle, а четырёхстрочный билдер цепляет одну спецификацию на каждую подстроку:
let spec = null;
for (const needle of needles) {
const s = new groupdocs.WithNameSpecification(needle, false /* fullMatch */);
spec = spec ? spec.or(s) : s;
}
return spec;
Три прохода переиспользуют этот билдер с разными списками. Сначала обрабатываем комментарии:
const affected = metadata.removeProperties(
nameContainsSpec(['Comment', 'Reviewer', 'Reviewed']));
metadata.save(outputPath);
Группа, описывающая временную шкалу редактирования, часто забывается, а именно она показывает, как был создан документ:
const affected = metadata.removeProperties(nameContainsSpec([
'Revision', 'TrackedChange', 'LastPrinted', 'TotalEditingTime', 'EditTime',
]));
metadata.save(outputPath);
Последний проход для SharePoint использует те же вызовы, но с Server, Workflow, Approver, ContentType и Template. Сопоставление по подстроке намеренно: оно захватывает CommentsCount вместе с Comment, без необходимости поддерживать точный список имён для каждого формата.
Step 3 - Wipe everything when selectivity stops helping
Для копии, которая покидает организацию, один вызов заменяет четыре предыдущих прохода:
const affected = metadata.sanitize();
metadata.save(outputPath);
sanitize() очищает каждый пакет метаданных, который обнаруживает библиотека, включая пользовательские части OOXML, и его счёт обычно превышает сумму целевых проходов. Он также удаляет Title и Subject, поэтому его следует вызывать на границе, а не внутри цикла проверки.
Step 4 - Verify, because a silent miss looks like success
Сканирование переиспользует те же спецификации через findProperties, который только читает. Результат — Java‑коллекция, поэтому её обход происходит по индексу:
const props = metadata.findProperties(tagSpec.or(nameSpec));
for (let i = 0; i < props.getCount(); i++) {
const p = props.get_Item(i);
const val = p.getValue && p.getValue();
const value = val ? String(val.getRawValue ? val.getRawValue() : val) : '';
if (!value || value === '0' || value === '0.0') continue;
leaks.push(`${p.getName()}=${value}`);
}
Фильтр пустых и нулевых значений заслуженно присутствует. Я добавил его после того, как один запуск провалился из‑за счётчика ревизий, обнулённого до 0, который сканер корректно отобразил как оставшееся свойство.
Complete Working Example
Репозиторий связывает шесть функций в index.js, который применяет лицензию, запускает каждый проход над resources/pii-sample.docx, проверяет существование всех выходных файлов и завершается проверкой, что список утечек пуст. Неудачная проверка завершает процесс с ненулевым кодом, поэтому всё работает как проверка в CI, а не как демонстрация для чтения.
Одна деталь стоит перенести в свою версию: каждый проход читает один и тот же исходный файл и пишет отдельный результат, а не передаёт очищенный файл в следующий проход. Это сохраняет независимость счётчиков, так что строка журнала для прохода комментариев отражает найденные им свойства, а не то, что осталось после правила идентификации.
When should I run a targeted pass instead of sanitize()?
Когда документ всё ещё используется. Файлы, находящиеся в обороте между рецензентами, полагаются на Title, Subject и Keywords для поиска и классификации, а sanitize() удаляет все три вместе с персональными данными. Запускайте проходы идентификации и комментариев во время совместной работы, сохраняйте описательные поля, а полное стирание оставляйте для копии, которая действительно покидает организацию.
Real-World Applications
Upload handler
Маршрут Express очищает вложение перед записью в хранилище, фиксирует количество затронутых свойств в тикете и отклоняет загрузку, если список утечек не пуст.
Nightly export job
Рабочий процесс проходит по папке экспорта, применяет проходы идентификации и серверных полей и завершает задачу с ошибкой, а не просто пишет предупреждение, если документ всё ещё содержит персональные данные.
Pre‑publication gate
Шаг сборки очищает вложения документации перед релизом, используя sanitize(), поскольку в этих файлах нет необходимости сохранять их метаданные.
Best Practices and Tips
- Всегда записывайте в новый путь, чтобы оригинал оставался доступным для разрешения споров.
- Закрывайте объект метаданных в блоке
finally, особенно в циклах. - Объединяйте спецификации с помощью
.or()в пакетных заданиях; один открытый и один сохранённый файл лучше, чем четыре. - Логируйте количество затронутых свойств для каждого прохода, включая нули, чтобы было видно, что формат не распознан.
Troubleshooting Common Issues
The affected count is zero on a document you know is dirty
Убедитесь, что входной формат распознан, прежде чем делать вывод о чистоте файла; нечитаемый файл и чистый файл дают одинаковый нулевой счёт.
The leak check reports properties you just removed
Указывайте путь к сохранённому выходному файлу, а не к входному. Сканирование читает тот файл, который ему передан.
Comment balloons are still visible in Word
Текст комментариев хранится в теле документа, а не в пакете метаданных. GroupDocs.Metadata удаляет свойства, связанные с комментариями; удаление самих облачков требует библиотеки для редактирования содержимого, например Aspose.Words.
Conclusion
Четыре шага, шесть функций, один скрипт, который сообщает, что он сделал. Спецификации по тегам охватывают группу идентификации во всех форматах, спецификации по имени покрывают те группы, которые теги не классифицируют, sanitize() обрабатывает границу, а сканирование утечек превращает всё в проверку. Клонируйте репозиторий, запустите его над документом, прошедшим реальный цикл рецензирования, и посмотрите на счётчики, прежде чем решать, какие проходы нужны вашему конвейеру.