💡 Полный рабочий пример доступен на GitHub:
office-metadata-pii-cleanup-nodejs

Введение

Точка загрузки принимает DOCX от сотрудника и сохраняет его к заявке клиента. Текст в порядке. Свойства — нет: в файле указаны имя автора, последний сохранявший, менеджер отдела из корпоративного шаблона и, поскольку документ пришёл из SharePoint, утверждающий, который его одобрил.

Санитайзер метаданных — небольшая программа, удаляющая эти свойства перед сохранением файла и проверяющая свою работу. В этом руководстве мы создаём такой скрипт на Node.js с помощью GroupDocs.Metadata за четыре шага: выбираем свойства по тегу, выбираем их по имени, стираем всё, когда выборка перестаёт помогать, и проверяем, что осталось. Каждый шаг состоит из нескольких строк, а готовый скрипт занимает менее ста строк.

Почему очистка метаданных важна

Данные накапливаются без какого‑либо выбора. Word записывает Author и LastSavedBy из учётной записи ОС при каждом сохранении, ведёт счётчик ревизий, отслеживает TotalEditingTime и фиксирует LastPrinted. Серверы документов добавляют пути рабочих процессов, идентификаторы утверждающих и URI типов контента при проверке‑в‑хранилище. Ни один из этих элементов не виден при чтении или печати документа, поэтому вычитка их никогда не обнаруживает.

Смысл делать это в Node.js, а не вручную, в том, что скрипт возвращает числа: каждый вызов удаления сообщает, сколько свойств было удалено, и это число можно записать в журнал, проверить в тесте или привязать к записи, к которой относится документ.

Есть и вторая причина, менее очевидная, пока не запущен пакетный процесс. Ручная очистка — решение, принимаемое один раз для каждого файла тем, кто в данный момент с ним работает, поэтому два человека, очищающие один и тот же тип документа, получат разные результаты. Скрипт фиксирует правило в одном месте: те же четыре тега, те же списки подстрок, применяемые одинаково, независимо от того, обрабатывает очередь три файла или три тысячи.

Предварительные требования

Пакет работает в Node.js через Java, поэтому на машине нужен Java‑runtime рядом с Node.

Установка

npm install @groupdocs/groupdocs.metadata

Пример проекта фиксирует версию 26.7 и добавляет запись overrides, задающую nan версии ^2.22.0, что позволяет собрать нативный биндинг на текущих релизах Node. Без файла лицензии библиотека работает в режиме оценки, чего достаточно для выполнения всех шагов здесь.

Шаг 1 — Выбор свойств по их смыслу

Имена свойств различаются между форматами и пакетами, поэтому первое правило сопоставляет по тегам. 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);

Ключевые моменты:

  • Четыре тега покрывают группу идентификации: создатель, редактор, менеджер и корпоративное поле компании.
  • Title, Subject и Keywords остаются нетронутыми, поэтому индекс записей, использующий их, продолжает работать.
  • removeProperties возвращает количество затронутых свойств, а не логическое значение.

Оборачивайте всё в try/finally с вызовом metadata.close() в блоке finally. Привязка держит файл открытым до этого момента, а цикл без закрытия быстро исчерпывает дескрипторы.

Шаг 2 — Выбор свойств по имени

В комментариях, счётчиках ревизий и полях сервера теги отсутствуют. Для них 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, не требуя поддерживать точный список имён для каждого формата.

Шаг 3 — Стирание всего, когда выборка перестаёт помогать

Для копии, покидающей организацию, один вызов заменяет четыре предыдущих прохода:

const affected = metadata.sanitize();
metadata.save(outputPath);

sanitize() очищает каждый пакет метаданных, который библиотека может обнаружить, включая пользовательские части OOXML, и обычно количество удалённых свойств превышает сумму целевых проходов. Он также удаляет Title и Subject, поэтому его следует вызывать на границе, а не внутри цикла проверки.

Шаг 4 — Проверка, потому что тихой пропуск выглядит как успех

Сканирование переиспользует те же спецификации через 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, который сканер корректно пометил как оставшееся свойство.

Полный рабочий пример

Репозиторий связывает шесть функций в index.js, который применяет лицензию, запускает каждый проход над resources/pii-sample.docx, проверяет существование всех выходных файлов и завершается проверкой, что список утечек пуст. Неудачное утверждение завершает процесс с ненулевым кодом, поэтому всё работает как проверка в CI, а не просто демонстрация.

Одна деталь стоит перенять в свой вариант: каждый проход читает один и тот же исходный файл и пишет отдельный результат, а не передаёт уже очищенный файл в следующий проход. Это сохраняет независимость счётчиков, так что строка журнала для прохода комментариев отражает найденные им свойства, а не то, что осталось после правила идентификации.

Когда следует запускать целевой проход вместо sanitize()?

Когда документ ещё используется. Файлы, циркулирующие между рецензентами, полагаются на Title, Subject и Keywords для поиска и классификации, а sanitize() удаляет все три вместе с персональными данными. Запускайте проходы идентификации и комментариев во время совместной работы, сохраняйте описательные поля, а полное стирание оставляйте для копии, покидающей организацию.

Применения в реальном мире

Обработчик загрузки

Маршрут Express очищает вложение перед записью в хранилище, фиксирует количество затронутых свойств в заявке и отклоняет загрузку, если список утечек не пуст.

Ночная задача экспорта

Рабочий процесс проходит по папке экспорта, применяет проходы идентификации и серверных полей и завершает задачу с ошибкой, а не просто пишет предупреждение, если документ всё ещё содержит остаточные PII.

Шлюз перед публикацией

Шаг сборки очищает вложения документации перед релизом, используя sanitize(), потому что в этих файлах нет необходимости сохранять их метаданные.

Лучшие практики и советы

  • Всегда записывайте в новый путь, чтобы оригинал оставался доступным для разрешения споров.
  • Закрывайте объект метаданных в блоке finally, особенно в циклах.
  • Объединяйте спецификации с помощью .or() в пакетных задачах; один открытый и один сохранённый файл лучше, чем четыре отдельных.
  • Логируйте количество затронутых свойств для каждого прохода, включая нули, чтобы было видно, что формат не распознан.

Устранение распространённых проблем

Количество затронутых свойств равно нулю, хотя документ явно «грязный»
Убедитесь, что входной формат распознан, прежде чем делать вывод о чистоте; нечитаемый файл и чистый дают одинаковый ноль.

Проверка утечек сообщает свойства, которые только что удалили
Указывайте путь к сохранённому выходному файлу, а не к входному. Сканер читает тот файл, который ему передан.

В Word всё ещё видны облачка комментариев
Текст комментариев хранится в теле документа, а не в пакете метаданных. GroupDocs.Metadata удаляет свойства, связанные с комментариями; удаление самих облачков требует библиотеки для редактирования содержимого, например Aspose.Words.

Заключение

Четыре шага, шесть функций, один скрипт, который сообщает, что он сделал. Спецификации по тегам покрывают группу идентификации во всех форматах, спецификации по имени охватывают те семейства, которые теги не классифицируют, sanitize() обрабатывает границу, а сканирование утечек превращает всё в проверку. Клонируйте репозиторий, запустите его на документе, прошедшем реальный цикл рецензирования, и посмотрите на счётчики, прежде чем решать, какие проходы нужны вашему конвейеру.

Дополнительные ресурсы