💡 Полный рабочий пример доступен на 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 Inspector → Remove 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‑документов: сохраняйте описательные метаданные, пока файл в обороте, стирайте всё, когда он выходит, и доказывайте результат в любом случае. Склонируйте пример, запустите его на документе, прошедшем реальный раунд рецензирования, и посмотрите количество затронутых свойств. Обычно оно выше, чем ожидалось, и в этом весь смысл.