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

Старый способ был болезненным

Рутина выглядит так. Открываете документ, File → Info → Check for Issues → Inspect Document, ставите галочки, Remove All, сохраняете под новым именем, закрываете, открываете следующий. Через сорок файлов кто‑то замечает, что Inspect Document также удалил Title, который используется в индексе записей, а копия, отправленная час назад, всё ещё содержит идентификатор одобряющего в SharePoint, потому что файл был сохранён из другого приложения, которое записало поле обратно.

Удаление PII из метаданных — это возможность GroupDocs.Metadata для .NET, которая программно удаляет свойства, содержащие идентифицирующую информацию, из Office‑документов и сообщает, что осталось после этого. Ручная процедура терпит неудачу по трем причинам: она не масштабируется более чем на несколько файлов, она «всё или ничего» в отношении того, какие поля удалять, и не оставляет записи о том, что было удалено. В этой статье показана версия на .NET, выполняющая ту же работу, по одной группе свойств за раз.

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

Есть лучший способ

Всё в GroupDocs.Metadata для .NET проходит через один поисковый движок свойств. RemoveProperties принимает лямбда‑выражение над MetadataProperty, удаляет каждое свойство, которое лямбда принимает, и возвращает количество удалённых. FindProperties запускает ту же лямбду без записи. Свойства также несут теги, поэтому Tags.Person.Creator идентифицирует поля типа «автор» во всех форматах и пакетах, вместо того чтобы сравнивать буквальные имена, которые различаются в разных приложениях.

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

Новый способ: предикат для каждой группы свойств

Шаг 1 — Очистить имена

Четыре проверки тегов покрывают группу идентификации. Описательные поля остаются нетронутыми, что отличает этот подход от Document InspectorRemove All:

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.RemoveProperties(p =>
        p.Tags.Contains(Tags.Person.Creator) ||
        p.Tags.Contains(Tags.Person.Editor) ||
        p.Tags.Contains(Tags.Person.Manager) ||
        p.Tags.Contains(Tags.Corporate.Company));
    metadata.Save(outputPath);
    return affected;
}

Проверка FileFormat.Unknown — это защита, которая сохраняет ноль честным: без неё нечитаемый файл и чистый файл выглядят одинаково для вызывающего кода.

Шаг 2 — Очистить семейства вокруг них

У комментариев, ревизий и серверных полей нет тега, поэтому предикат сравнивает имена. Временная шкала редактирования — самая часто забываемая группа, и именно она показывает, как был создан документ:

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.RemoveProperties(p =>
        p.Name != null && (
            p.Name.Contains("Revision") ||
            p.Name.Contains("TrackedChange") ||
            p.Name.Contains("LastPrinted") ||
            p.Name.Contains("TotalEditingTime") ||
            p.Name.Contains("EditTime")));
    metadata.Save(outputPath);
    return affected;
}

Поиск подстрок намеренный: он захватывает CommentsCount вместе с Comment и TotalEditingTime вместе с EditTime, без необходимости поддерживать точный список имён для каждого формата. Проход для SharePoint — тот же вызов, но с Server, Workflow, Approver, ContentType и Template.

Шаг 3 — Стереть всё, затем проверить результат

На границе доверия один вызов заменяет четыре предыдущих прохода:

using (var metadata = new Metadata(inputPath))
{
    if (metadata.FileFormat == FileFormat.Unknown) return 0;
    var affected = metadata.Sanitize();
    metadata.Save(outputPath);
    return affected;
}

Затем часть, которой у ручной процедуры нет аналога. Сканирование‑проверка повторно использует предикаты удаления через FindProperties и распределяет оставшиеся свойства по двум спискам:

foreach (var p in props)
{
    var value = p.InterpretedValue?.ToString() ?? p.Value?.ToString() ?? string.Empty;
    if (string.IsNullOrWhiteSpace(value)) continue;
    if (value == "0" || value == "0.0") continue;

    var entry = $"{p.Name}={value}";
    var name = p.Name ?? string.Empty;
    if (name.StartsWith("Comment") || name.StartsWith("Revision"))
        report.ContentLevelLeaks.Add(entry);
    else
        report.MetadataLeaks.Add(entry);
}

MetadataLeaks должен быть пустым, чтобы файл считался «очищенным». ContentLevelLeaks — информационный список: авторы комментариев и изменений в Word находятся в word/document.xml, что является содержимым тела, и их удаление требует библиотеки для редактирования контента, такой как Aspose.Words, а не API метаданных.

Почему нельзя просто вызвать Sanitize() для всего?

Потому что большинство документов всё ещё находятся в использовании. Sanitize() очищает каждый обнаруженный пакет, включая Title, Subject и Keywords — поля, от которых зависят системы учёта записей и поисковые индексы. Используйте целевые проходы, пока файл циркулирует внутри организации, сохраняйте описательные метаданные, а полное стирание оставляйте для копии, которая действительно покидает компанию.

Сравнение «до» и «после»

Ручная проверка GroupDocs.Metadata для .NET
Выборочность Remove All, включены описательные поля по одному предикату на группу свойств
Охват только те поля, которые показывает диалог каждый пакет, который обнаруживает библиотека, включая пользовательские части OOXML
Запись нет количество затронутых, возвращаемое каждой операцией
Проверка открыть заново и посмотреть сканирование FindProperties с метаданными и списками уровня контента
Пакет из 200 файлов 200 последовательных кликов один цикл, пять операций, одна строка журнала на файл

Строка, меняющая поведение, — это запись. Как только каждый проход возвращает число, очистка перестаёт быть «шагом, который кто‑то помнит выполнить», и становится данными, которые конвейер может проверять: порог в тесте, поле в таблице аудита, условие, которое падает в ночном задании. Это также строка, которую ручная процедура не может обеспечить при любой дисциплине.

Реальный пример: хук перед отправкой

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

Два момента делают такой хук практичным. Проходы записывают результат в новый путь, поэтому оригинал остаётся в личном хранилище сотрудника и ничего не уничтожается автоматическим шагом. А количества сохраняются в записи тикета рядом с вложением, что значит ответ на вопрос «что было удалено из этого документа» — это сохранённое число, а не предположение о том, что обычно делает конвейер.

В первый раз, когда я применил эту проверку к реальному шаблону, она вернула значение Manager, которое проход идентификации удалил секунды назад, а корпоративный шаблон записал обратно при сохранении. Вызов удаления работал точно как задокументировано; проблема была в конвейере вокруг него, и только повторное чтение это показало.

Что ещё можно сделать с GroupDocs.Metadata?

Тот же движок предикатов читает. Сравнение свойств между двумя версиями документа выявляет изменения владельцев и переавторизацию вне процесса рецензирования, а обзор очистки метаданных описывает, где интерактивный инструмент всё ещё уместен рядом с API‑ориентированным проходом. Поскольку система тегов охватывает форматы, предикат идентификации, написанный здесь, работает и с PDF, изображениями, и аудиофайлами без изменений.

Эта переносимость стоит планировать. Правило очистки, написанное как лямбда‑выражение над MetadataProperty, — обычный C#, поэтому его можно разместить в общей библиотеке, покрыть юнит‑тестами на тестовых документах и применять в любом сервисе, который в этом нуждается: конечной точке экспорта, запланированном задании по учёту записей или шаге сборки, который очищает вложения документации перед релизом. Правила находятся в одном месте; меняются только места вызова.

Заключение

Четыре целевых прохода, один полный Sanitize, одно сканирование‑проверка. Этот набор покрывает практический диапазон для Office‑документов: сохраняйте описательные метаданные, пока файл в обороте, стирайте всё, когда он выходит, и доказывайте результат в любом случае. Склонируйте пример, запустите его на документе, прошедшем реальный раунд рецензирования, и посмотрите количество затронутых свойств. Обычно оно выше, чем ожидалось, и в этом весь смысл.

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