💡 Полный рабочий пример доступен на GitHub:
remove-pii-from-office-metadata-java
Проблема соответствия: почему ручной обзор метаданных не масштабируется
Команда по работе с записями отправляет 200 документов внешнему аудитору. Кто‑то прочитал каждую страницу. Никто не читал свойства, а именно в свойствах находятся персональные данные: аналитик в Author, второй аналитик в LastSavedBy, руководитель отдела в Manager, дочерняя компания в Company, метка времени LastPrinted с ночи перед дедлайном и, если документ прошёл через SharePoint, идентификатор утверждающего и путь рабочего процесса.
Очистка метаданных — это рабочий процесс GroupDocs.Metadata для Java, который удаляет свойства, содержащие идентифицирующую информацию, из файлов Word, Excel и PowerPoint, а затем считывает результат, чтобы сообщить, что осталось. В этой статье мы пройдёмся по рабочему процессу, как его построила бы команда по соответствию: какие группы свойств существуют, какое правило удаления подходит каждой из них, когда полное стирание заменяет целевые проходы и почему проверка должна входить в ту же задачу, а не в чек‑лист.
Проблема масштаба — не в том, что удаление сложно. Проблема в том, что ручной обзор не оставляет записи. Аудитор, спрашивая «какие поля были удалены из этого файла и когда», требует цифры, а диалоговое окно свойств её не выдаёт.
Почему здесь не работают универсальные инструменты очистки
Диалоговое окно свойств Windows редактирует по одному файлу и охватывает лишь часть полей. Инспектор документов Office работает интерактивно, что исключает его из ночных задач. Оба оставляют нетронутыми пользовательские части OOXML и не записывают ничего, что конвейер мог бы прочитать обратно.
Команды, которые спускаются на уровень ниже и редактируют docProps/core.xml и docProps/custom.xml напрямую, берут на себя нагрузку по обслуживанию: XPath‑выражение на каждое поле, на каждый формат, которое нужно пересматривать каждый раз, когда приложение‑производитель меняет имя. Такая работа также неправильно решает вопрос классификации. Имена свойств различаются в разных пакетах, поэтому список имён тихо устаревает, а правило, которое больше не совпадает, выглядит так же, как файл, уже очищенный.
Решение: GroupDocs.Metadata в рабочем процессе с записями
GroupDocs.Metadata для Java пропускает всё через один поисковый движок свойств. Объект Specification решает, какие свойства подходят, removeProperties удаляет каждое совпадение и возвращает количество затронутых, а findProperties выполняет тот же предикат в режиме только чтения. Свойства несут теги, поэтому Tags.getPerson().getCreator() определяет поля типа «автор», независимо от формата или пакета, из которого они пришли.
В Java нет перегрузки лямбда‑выражения для removeProperties, что здесь оказывается преимуществом: каждое правило — это объект, а объекты переиспользуемы. Тот же экземпляр спецификации, который очищает группу, можно передать сканированию проверки, поэтому проверка не отклоняется от очистки, которую должна тестировать.
Реализация этапа очистки конвейера шаг за шагом
Шаг 1 — Очистить группу идентификации по тегу
Четыре спецификации тегов, объединённые через .or(...), покрывают создателя, редактора, менеджера и компанию. Ничего другого не перемещается, поэтому Title, Subject и Keywords остаются доступными для индекса записей.
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(
new ContainsTagSpecification(Tags.getPerson().getCreator())
.or(new ContainsTagSpecification(Tags.getPerson().getEditor()))
.or(new ContainsTagSpecification(Tags.getPerson().getManager())
.or(new ContainsTagSpecification(Tags.getCorporate().getCompany())));
metadata.save(outputPath);
return affected;
}
Защита FileFormat.Unknown важнее, чем кажется. Без неё нечитаемый файл возвращает ноль удалений, что вызывающий код не может отличить от документа, пришедшего уже чистым.
Шаг 2 — Очистить семейства полей по имени
Тематические ветки комментариев, счётчики ревизий и серверные поля не имеют тегов, поэтому они сопоставляются по имени. Небольшой подкласс Specification принимает список подстрок varargs, позволяя одному классу обслуживать три прохода:
public class NameContainsSpec extends Specification {
private final String[] needles;
public NameContainsSpec(String... needles) {
this.needles = needles;
}
@Override
public boolean isSatisfiedBy(MetadataProperty candidate) {
String name = candidate.getName();
if (name == null) return false;
for (String n : needles) {
if (name.contains(n)) return true;
}
return false;
}
}
Временная шкала редактирования — это то, что интересует группы соответствия, потому что количество ревизий и дата последней печати описывают, как был создан документ:
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.removeProperties(new NameContainsSpec(
"Revision", "TrackedChange", "LastPrinted", "TotalEditingTime", "EditTime"));
metadata.save(outputPath);
return affected;
}
Проход комментариев и проход SharePoint — это один и тот же вызов с разными списками подстрок: Comment, Reviewer, Reviewed для следов рецензирования и Server, Workflow, Approver, ContentType, Template для полей серверов документов.
Шаг 3 — Стереть на границе
Когда файл покидает организацию, избирательность перестаёт иметь смысл. Один вызов очищает каждый пакет метаданных, который обнаруживает библиотека, включая пользовательские части OOXML, которые не покрыты целевыми предикатами:
try (Metadata metadata = new Metadata(inputPath)) {
if (metadata.getFileFormat() == FileFormat.Unknown) return 0;
int affected = metadata.sanitize();
metadata.save(outputPath);
return affected;
}
Шаг 4 — Проверить и классифицировать оставшееся
Сканирование выполняет объединение всех правил через findProperties и распределяет найденные элементы по двум спискам. Пустые значения и нулевые счётчики пропускаются, а записи, имена которых начинаются с Comment, Revision или Inspection, являются обёртками над содержимым тела, а не над метаданными:
String value = "";
if (p.getValue() != null && p.getValue().getRawValue() != null) {
value = String.valueOf(p.getValue().getRawValue());
}
if (value.isEmpty() || value.equals("0") || value.equals("0.0")) continue;
String entry = p.getName() + "=" + value;
String name = p.getName() == null ? "" : p.getName();
if (name.startsWith("Comment") || name.startsWith("Revision")
|| name.startsWith("Inspection")) {
report.contentLevelLeaks.add(entry);
} else {
report.metadataLeaks.add(entry);
}
Разделение — это то, что сохраняет честность сигнала «пройдено/не пройдено». Утечки метаданных должны быть пустыми. Утечки уровня содержимого остаются информационными, потому что комментарии Word и авторы отслеживаемых изменений находятся внутри word/document.xml, а библиотека метаданных лишь сообщает о них, не редактируя; их удаление требует библиотеки редактирования содержимого, такой как Aspose.Words.
Когда целевой проход лучше полного стирания?
Когда документ всё ещё используется. Файл, циркулирующий между рецензентами, нуждается в Title, Subject и Keywords для поиска и классификации записей, а sanitize() удаляет их все. Выполняйте проходы идентификации и комментариев во время совместной работы, сохраняйте описательные поля и оставляйте полное стирание для момента, когда файл переходит к внешней стороне.
Реальный рабочий процесс: экспортная задача для внешнего аудитора
Представьте ночную задачу. Она читает список идентификаторов документов, копирует каждый файл во временную папку, применяет проход идентификации и серверный проход, вызывает sanitize() для всего, что помечено как покидающее организацию, затем запускает проверку утечек против сохранённой копии. Каждый шаг добавляет своё количество затронутых к одной строке журнала на файл, и непустой список утечек метаданных приводит к провалу задачи, а не к простому предупреждению.
Однажды я провёл полдня над версией этой задачи, которая сообщала о нуле удалений в 40 файлах и выглядела как чистая партия. В папке входных данных находились устаревшие бинарные .doc, защита формата возвращала ранний выход для каждого из них, и в журнале ничто не отличало «ничего не нужно удалять» от «ничего не прочитано». Добавление формата в журнал вместе с количеством исправило ситуацию.
Влияние на бизнес: что меняет этот подход
| Aspect | Manual review | GroupDocs.Metadata pipeline |
|---|---|---|
| Coverage | fields visible in the properties dialog | every package the library detects, including custom OOXML parts |
| Record of the work | notes, if anyone wrote them | affected count per file and per property group |
| Repeatability | depends on who did it | one specification per rule, applied identically to every file |
| Verification | reopen the file and look | findProperties scan with a two-way classification |
| Scale | one file at a time | the same six operations run in a loop over an export folder |
Другие сценарии, где подходит GroupDocs.Metadata
Тот же движок свойств читает так же хорошо, как и удаляет. Сравнение метаданных между двумя версиями документа показывает, какие изменения внесло конкретное редактирование, что полезно при спорах о праве собственности и при поиске файла, переавторенного вне процесса. Работа с metadata tags вместо имён делает оба случая переносимыми между DOCX, XLSX, PPTX, PDF и форматами изображений.
Начало работы с GroupDocs.Metadata для Java
Добавьте репозиторий GroupDocs Java в pom.xml и укажите зависимость com.groupdocs:groupdocs-metadata. Библиотека работает в режиме оценки без лицензии, чего достаточно для выполнения всех шести операций над образцом DOCX и просмотра счётчиков. Начните с прохода идентификации, добавляйте проходы семейства полей по мере необходимости ваших источников документов и внедрите проверку утечек до того, как что‑либо попадёт в продакшн.