💡 Полный рабочий пример доступен на 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 и просмотра счётчиков. Начните с прохода идентификации, добавляйте проходы семейства полей по мере необходимости ваших источников документов и внедрите проверку утечек до того, как что‑либо попадёт в продакшн.

Resources