💡 Full working example available on GitHub: scrub-office-document-pii-dotnet
El método antiguo era doloroso
La rutina funciona así. Abres el documento, Archivo, Información, Buscar problemas, Inspeccionar documento, marcas las casillas, Eliminar todo, guardas con un nombre nuevo, cierras, abres el siguiente. Cuarenta archivos después alguien se da cuenta de que Inspeccionar documento también eliminó el Título del que depende el índice de registros, y de que la copia enviada una hora antes todavía llevaba un ID de aprobador de SharePoint, porque el archivo se había guardado desde una aplicación diferente que volvió a escribir el campo.
La eliminación de PII de metadatos es una capacidad de GroupDocs.Metadata para .NET que borra programáticamente las propiedades que contienen información de identidad de documentos de Office y reporta lo que queda después. La rutina manual falla en tres aspectos: no escala más allá de unos pocos archivos, es todo o nada respecto a qué campos se eliminan, y no genera ningún registro de lo que se quitó. Este artículo muestra la versión .NET del mismo trabajo, un grupo de propiedades a la vez.
Ayuda saber qué hay realmente dentro. Un archivo de Word que ha pasado por una ronda de revisión suele contener Author y LastSavedBy de la cuenta de Windows de quien lo guardó, Manager y Company de la plantilla corporativa, un contador de revisiones, TotalEditingTime, una marca de tiempo LastPrinted y contadores de hilos de comentarios. Si añades SharePoint a la cadena también obtienes identificadores de aprobadores, rutas de flujo de trabajo, URIs de tipo de contenido y la plantilla con la que se creó el documento. Nada de eso es visible en la página, y todo viaja dentro del mismo archivo.
Hay una mejor manera
Todo en GroupDocs.Metadata para .NET pasa por un único motor de búsqueda de propiedades. RemoveProperties recibe una lambda sobre MetadataProperty, elimina cada propiedad que la lambda acepta y devuelve el recuento. FindProperties ejecuta la misma lambda sin escribir. Las propiedades también llevan etiquetas, de modo que Tags.Person.Creator identifica campos de tipo autor en todos los formatos y paquetes, en lugar de coincidir con nombres literales que difieren según la aplicación productora.
Eso brinda tres formas de limpieza en lugar de un solo botón: una pasada de etiquetas para campos de identidad, pasadas por nombre para familias como comentarios y revisiones, y Sanitize() cuando nada debe permanecer. Las tres devuelven números, y esos números hacen que la pasada sea auditable.
El nuevo método: un predicado por grupo de propiedades
Paso 1 - Limpiar los nombres
Cuatro comprobaciones de etiquetas cubren el grupo de identidad. Los campos descriptivos quedan intactos, que es la diferencia con Remove All del Inspector de documentos:
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;
}
La comprobación FileFormat.Unknown es la guardia que mantiene honesto un cero: sin ella, un archivo ilegible y un archivo limpio se ven iguales para el llamador.
Paso 2 - Limpiar las familias alrededor de ellos
Los comentarios, revisiones y campos del servidor no tienen etiqueta, por lo que el predicado coincide con los nombres. La línea de tiempo de edición es el grupo que más a menudo se olvida, y es el que indica cómo se produjo un documento:
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;
}
La coincidencia por subcadena es deliberada: captura CommentsCount junto a Comment, y TotalEditingTime junto a EditTime, sin mantener una lista exacta de nombres por formato. La pasada de SharePoint es la misma llamada con Server, Workflow, Approver, ContentType y Template.
Paso 3 - Eliminar todo, luego comprobar el resultado
En el límite de confianza, una llamada reemplaza las cuatro pasadas:
using (var metadata = new Metadata(inputPath))
{
if (metadata.FileFormat == FileFormat.Unknown) return 0;
var affected = metadata.Sanitize();
metadata.Save(outputPath);
return affected;
}
Luego la parte que la rutina manual no tiene equivalente. El escaneo de verificación reutiliza los predicados de eliminación mediante FindProperties y clasifica los supervivientes en dos listas:
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 debe estar vacío antes de que un archivo cuente como saneado. ContentLevelLeaks es informativo: los autores de comentarios y cambios rastreados de Word están en word/document.xml, que es contenido del cuerpo, y limpiar esos elementos requiere una biblioteca de edición de contenido como Aspose.Words en lugar de una API de metadatos.
¿Por qué no simplemente llamar a Sanitize en todo?
Porque la mayoría de los documentos siguen en uso. Sanitize() elimina cada paquete detectado, e incluye Título, Asunto y Palabras clave, campos de los que dependen los sistemas de registros y los índices de búsqueda. Usa las pasadas dirigidas mientras el archivo circula internamente, mantén los metadatos descriptivos funcionando, y reserva la eliminación total para la copia que realmente sale de la organización.
Comparación lado a lado: Antes vs. Después
| Inspección manual | GroupDocs.Metadata para .NET | |
|---|---|---|
| Selectividad | Eliminar todo, incluidos los campos descriptivos | un predicado por grupo de propiedades |
| Cobertura | campos que expone el diálogo | cada paquete que detecta la biblioteca, partes OOXML personalizadas incluidas |
| Registro | ninguno | recuento de afectados devuelto por cada operación |
| Verificación | volver a abrir y mirar | escaneo FindProperties con listas de metadatos y de nivel de contenido |
| Lote de 200 archivos | 200 clics | un bucle, cinco operaciones, una línea de registro por archivo |
La fila que cambia el comportamiento es la del registro. Una vez que cada pasada devuelve un recuento, la sanitización deja de ser un paso que alguien recuerda hacer y se convierte en datos que la canalización puede afirmar: un umbral en una prueba, un campo en una tabla de auditoría, una condición que falla un trabajo nocturno. Esa también es la fila que una rutina manual no puede producir bajo ningún nivel de disciplina.
Ejemplo del mundo real: El gancho previo al envío
Un portal de soporte permite al personal adjuntar documentos a tickets de cliente. El manejador de adjuntos ahora ejecuta la pasada de identidad y la pasada de servidor antes de que el archivo se almacene, registra ambos recuentos en el ticket y ejecuta la comprobación de fugas en la copia guardada. Una lista de fugas de metadatos no vacía rechaza la carga con un mensaje que nombra la propiedad infractora, de modo que la persona que adjunta el archivo lo descubre inmediatamente en lugar de después de que llegue al cliente.
Dos detalles hacen que ese gancho sea práctico. Las pasadas escriben en una ruta nueva, por lo que el original permanece en el almacenamiento del empleado y nada se destruye mediante un paso automatizado. Y los recuentos se guardan en el registro del ticket junto al adjunto, lo que significa que la respuesta a “qué se eliminó de este documento” es un número almacenado y no una suposición sobre lo que la canalización suele hacer.
La primera vez que apliqué esa comprobación a una plantilla real, volvió con un valor Manager que la pasada de identidad había eliminado segundos antes y que la plantilla corporativa había escrito de nuevo al guardar. La llamada de eliminación funcionó exactamente como se documenta; el problema estaba en la canalización que la rodea, y solo la lectura posterior lo mostró.
¿Qué más puedes hacer con GroupDocs.Metadata?
El mismo motor de predicados lee. Comparar propiedades entre dos versiones de un documento revela cambios de titularidad y reautoría fuera del proceso de revisión, y la visión general de limpieza de metadatos cubre dónde una herramienta interactiva aún encaja junto a una pasada impulsada por API. Como el sistema de etiquetas abarca formatos, el predicado de identidad escrito aquí también funciona contra PDFs, imágenes y archivos de audio sin modificaciones.
Esa portabilidad vale la pena planificarla. Una regla de limpieza escrita como lambda sobre MetadataProperty es C# ordinario, por lo que puede vivir en una biblioteca compartida, probarse unitariamente contra documentos de muestra y aplicarse por cualquier servicio que lo necesite: un endpoint de exportación, un trabajo programado de registros, o un paso de compilación que sanea adjuntos de documentación antes del lanzamiento. Las reglas permanecen en un solo lugar; solo cambian los sitios de llamada.
Conclusión
Cuatro pasadas dirigidas, una sanitización completa, un escaneo de verificación. Ese conjunto cubre el rango práctico para documentos de Office: mantener los metadatos descriptivos mientras el archivo circula, eliminar todo cuando sale, y demostrar el resultado en cualquier caso. Clona el ejemplo, ejecútalo contra un documento que haya pasado por una ronda de revisión real y lee los recuentos de afectados. Usualmente son más altos de lo esperado, que es precisamente el objetivo.