Sistema de Anonimización de Texto - Solución Completa
Sistema de Anonimización de Texto - Solución Completa
Nota importada desde Inbox durante consolidacion bulk.
Resumen
Arquitectura y código Python completo para un sistema de anonimización de texto con tres capas de detección complementarias: Capa 1 (NER con transformers/Hugging Face para nombres, lugares, organizaciones), Capa 2 (Regex determinista para DNI, email, teléfono, IBAN, NIE, códigos postales, fechas), y Capa 3 (LLM con Ollama/llama3.2:3b para entidades complejas que requieren contexto como datos médicos, judiciales, edades). Las tres capas se unifican con resolución de solapamientos por prioridad (regex > ner > llm) y se aplican reglas de reemplazo configurables que generan un mapa reversible. Incluye 6 módulos Python completos: config.py, detector_ner.py, detector_regex.py, detector_llm.py, anonimizador.py y main.py.
Definición
Sistema de anonimización de texto que recibe un documento con datos sensibles y devuelve el texto con los datos reemplazados por tokens categorizados, sin conexión a internet obligatoria (LLM local), de forma inteligente usando tres capas complementarias de detección.
Ejemplo de entrada:
El paciente Juan Carlos Martínez, DNI 45678912B, domiciliado en
Calle Mayor 15, Madrid. Contacto: juan@gmail.com, tel 612345678.
Trabaja en Mapfre. IBAN: ES91 2100 0418 4502 0005 1332.
Ejemplo de salida:
El paciente [PERSONA_1], [DNI_REDACTADO], domiciliado en
[DIRECCIÓN_REDACTADA]. Contacto: [EMAIL_1], tel [TEL_REDACTADO].
Trabaja en [ORG_1]. IBAN: [CUENTA_REDACTADA].
Contexto
Arquitectura: 3 Capas de Detección
Cada capa cubre las debilidades de las otras:
TEXTO DE ENTRADA
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ CAPA 1 │ │ CAPA 2 │ │ CAPA 3 │
│ NER │ │ Regex │ │ LLM (Ollama) │
│ (rápido) │ │ (exacto) │ │ (inteligente)│
└────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
└─────────────┼──────────────┘
▼
ENTIDADES UNIFICADAS
│
▼
┌───────────────────┐
│ REGLAS DE │
│ REEMPLAZO │
└────────┬──────────┘
│
▼
TEXTO ANONIMIZADO
+ MAPA REVERSIBLE
| Capa | Detecta | Fortaleza | Debilidad |
|---|---|---|---|
| NER | Nombres, lugares, organizaciones | Muy rápido, alta precisión en español | Solo 3-4 categorías |
| Regex | DNI, email, teléfono, IBAN | 100% determinista, nunca falla | No entiende contexto |
| LLM | Todo lo anterior + contexto complejo | Entiende directrices, detecta lo ambiguo | Más lento, puede alucinar |
Aplicación
Dependencias
# NER (Capa 1)
pip install transformers torch
# LLM (Capa 3)
# Instalar Ollama desde https://ollama.com
ollama pull llama3.2:3b
Código Completo
config.py - Configuración centralizada
# Modelo NER de Hugging Face (se descarga una vez, ~500MB)
NER_MODEL = "Davlan/bert-base-multilingual-cased-ner-hrl"
# Modelo LLM en Ollama
LLM_MODEL = "llama3.2:3b"
OLLAMA_URL = "http://localhost:11434/api/generate"
# Mapeo de etiquetas NER a tipos internos
NER_LABEL_MAP = {
"PER": "PERSONA",
"LOC": "DIRECCION",
"ORG": "ORGANIZACION",
}
# Patrones regex por tipo de entidad
REGEX_PATTERNS = {
"EMAIL": r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
"TELEFONO": r'\b[67]\d{8}\b',
"TELEFONO_INTL": r'\b\+34[\s.-]?[67]\d{8}\b',
"DNI": r'\b\d{8}[A-Za-z]\b',
"NIE": r'\b[XYZxyz]\d{7}[A-Za-z]\b',
"IBAN": r'\b[A-Z]{2}\d{2}[\s]?\d{4}[\s]?\d{4}[\s]?\d{4}[\s]?\d{4}[\s]?\d{4}\b',
"CODIGO_POSTAL": r'\b\d{5}\b',
"FECHA": r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b',
}
# Reglas de reemplazo por tipo
REGLAS_REEMPLAZO = {
"PERSONA": lambda i, txt: f"[PERSONA_{i}]",
"DIRECCION": lambda i, txt: "[DIRECCIÓN_REDACTADA]",
"ORGANIZACION": lambda i, txt: f"[ORG_{i}]",
"EMAIL": lambda i, txt: f"[EMAIL_{i}]",
"TELEFONO": lambda i, txt: "[TEL_REDACTADO]",
"TELEFONO_INTL": lambda i, txt: "[TEL_REDACTADO]",
"DNI": lambda i, txt: "[DNI_REDACTADO]",
"NIE": lambda i, txt: "[NIE_REDACTADO]",
"IBAN": lambda i, txt: "[CUENTA_REDACTADA]",
"CODIGO_POSTAL": lambda i, txt: "[CP_REDACTADO]",
"FECHA": lambda i, txt: "[FECHA_REDACTADA]",
"EDAD": lambda i, txt: "[EDAD_REDACTADA]",
}
detector_ner.py - Capa 1: NER con transformers
from transformers import pipeline
from config import NER_MODEL, NER_LABEL_MAP
_ner_pipeline = None
def cargar_ner():
global _ner_pipeline
if _ner_pipeline is None:
_ner_pipeline = pipeline(
"ner",
model=NER_MODEL,
aggregation_strategy="simple"
)
return _ner_pipeline
def detectar_ner(texto: str) -> list:
"""Detecta personas, lugares y organizaciones."""
ner = cargar_ner()
resultados = ner(texto)
entidades = []
for r in resultados:
tipo = NER_LABEL_MAP.get(r["entity_group"])
if tipo and r["score"] > 0.75:
entidades.append({
"texto": texto[r["start"]:r["end"]],
"tipo": tipo,
"inicio": r["start"],
"fin": r["end"],
"score": round(r["score"], 3),
"fuente": "ner",
})
return entidades
detector_regex.py - Capa 2: Patrones regex
import re
from config import REGEX_PATTERNS
def detectar_regex(texto: str) -> list:
"""Detecta entidades con patrones predecibles: DNI, email, tel, IBAN..."""
entidades = []
for tipo, patron in REGEX_PATTERNS.items():
for match in re.finditer(patron, texto):
entidades.append({
"texto": match.group(),
"tipo": tipo,
"inicio": match.start(),
"fin": match.end(),
"score": 1.0,
"fuente": "regex",
})
return entidades
detector_llm.py - Capa 3: LLM con Ollama
import json
import requests
from config import LLM_MODEL, OLLAMA_URL
DIRECTRICES = """
Eres un sistema de detección de datos personales para anonimización.
DEBES detectar:
- PERSONA: nombres completos o parciales de personas físicas
- DIRECCION: direcciones postales completas (calle, número, piso, ciudad)
- ORGANIZACION: empresas u organizaciones cuando identifican al individuo
- FECHA_NACIMIENTO: fechas de nacimiento explícitas
- EDAD: edad cuando combinada con otros datos puede identificar a alguien
- DATO_MEDICO: diagnósticos, tratamientos, condiciones de salud
- DATO_JUDICIAL: números de expediente, juzgados, sentencias
NO marques:
- Países o regiones genéricas ("España", "Europa")
- Cargos profesionales genéricos ("el médico", "la abogada")
- Datos ya detectables por regex (email, teléfono, DNI, IBAN)
- Información pública general
Responde SOLO con JSON válido:
{"entidades": [{"texto": "texto exacto", "tipo": "TIPO", "inicio": 0, "fin": 10}]}
Si no hay entidades: {"entidades": []}
"""
def detectar_llm(texto: str) -> list:
"""Detecta entidades complejas que requieren comprensión del contexto."""
try:
response = requests.post(OLLAMA_URL, json={
"model": LLM_MODEL,
"prompt": f"{DIRECTRICES}\n\nTexto:\n\"{texto}\"",
"stream": False,
"options": {"temperature": 0, "num_predict": 2048}
}, timeout=30)
raw = response.json()["response"]
inicio = raw.find("{")
fin = raw.rfind("}") + 1
if inicio == -1 or fin == 0:
return []
resultado = json.loads(raw[inicio:fin])
entidades = []
for e in resultado.get("entidades", []):
pos = texto.find(e["texto"])
if pos != -1:
entidades.append({
"texto": e["texto"],
"tipo": e["tipo"],
"inicio": pos,
"fin": pos + len(e["texto"]),
"score": 0.8,
"fuente": "llm",
})
return entidades
except (requests.RequestException, json.JSONDecodeError, KeyError):
return []
anonimizador.py - Motor principal
from config import REGLAS_REEMPLAZO
from detector_ner import detectar_ner
from detector_regex import detectar_regex
from detector_llm import detectar_llm
def unificar_entidades(lista_ner: list, lista_regex: list, lista_llm: list) -> list:
"""
Combina entidades de las 3 fuentes.
Si hay solapamiento, prioriza: regex > ner > llm
"""
todas = []
for e in lista_regex:
e["prioridad"] = 1
todas.append(e)
for e in lista_ner:
e["prioridad"] = 2
todas.append(e)
for e in lista_llm:
e["prioridad"] = 3
todas.append(e)
todas.sort(key=lambda e: (e["inicio"], e["prioridad"]))
filtradas = []
ultimo_fin = -1
for e in todas:
if e["inicio"] >= ultimo_fin:
filtradas.append(e)
ultimo_fin = e["fin"]
return filtradas
def aplicar_reglas(texto: str, entidades: list) -> dict:
"""Aplica las reglas de reemplazo sobre el texto."""
entidades_rev = sorted(entidades, key=lambda e: e["inicio"], reverse=True)
resultado = texto
mapa = {}
contadores = {}
for ent in entidades_rev:
tipo = ent["tipo"]
original = ent["texto"]
contadores[tipo] = contadores.get(tipo, 0) + 1
regla = REGLAS_REEMPLAZO.get(tipo, lambda i, t: f"[{tipo}_{i}]")
reemplazo = regla(contadores[tipo], original)
resultado = resultado[:ent["inicio"]] + reemplazo + resultado[ent["fin"]:]
mapa[reemplazo] = original
return {
"original": texto,
"anonimizado": resultado,
"mapa_reversible": mapa,
"entidades": entidades,
"total_detectadas": len(entidades),
}
def anonimizar(texto: str, usar_llm: bool = True) -> dict:
"""Pipeline completo de anonimización."""
ents_ner = detectar_ner(texto)
ents_regex = detectar_regex(texto)
ents_llm = detectar_llm(texto) if usar_llm else []
entidades = unificar_entidades(ents_ner, ents_regex, ents_llm)
return aplicar_reglas(texto, entidades)
main.py - Punto de entrada
from anonimizador import anonimizar
texto = """
Informe médico del paciente Juan Carlos Martínez García, de 45 años,
con DNI 45678912B, domiciliado en Calle Mayor 15, 3ºA, 28013 Madrid.
Diagnóstico: diabetes tipo 2 en tratamiento con metformina.
Contacto: juancarlos.martinez@gmail.com, teléfono 612345678.
Empleado de Seguros Mapfre desde 2015.
Cuenta para facturación: ES91 2100 0418 4502 0005 1332.
Expediente judicial 1234/2023 del Juzgado nº5 de Madrid.
"""
resultado = anonimizar(texto)
print(resultado["anonimizado"])
print(f"\nEntidades detectadas: {resultado['total_detectadas']}")
print("\nDetalle:")
for e in resultado["entidades"]:
print(f" [{e['fuente']:>5}] {e['tipo']:<20} → \"{e['texto']}\"")
Salida esperada:
Informe médico del paciente [PERSONA_1], de [EDAD_REDACTADA],
con [DNI_REDACTADO], domiciliado en [DIRECCIÓN_REDACTADA], [CP_REDACTADO] [DIRECCIÓN_REDACTADA].
Diagnóstico: [DATO_MEDICO_1] en tratamiento con [DATO_MEDICO_2].
Contacto: [EMAIL_1], teléfono [TEL_REDACTADO].
Empleado de [ORG_1] desde 2015.
Cuenta para facturación: [CUENTA_REDACTADA].
[DATO_JUDICIAL_1].
Entidades detectadas: 12
Detalle:
[ ner] PERSONA → "Juan Carlos Martínez García"
[ llm] EDAD → "45 años"
[regex] DNI → "45678912B"
[ ner] DIRECCION → "Calle Mayor 15, 3ºA"
[regex] CODIGO_POSTAL → "28013"
[ ner] DIRECCION → "Madrid"
[ llm] DATO_MEDICO → "diabetes tipo 2"
[ llm] DATO_MEDICO → "metformina"
[regex] EMAIL → "juancarlos.martinez@gmail.com"
[regex] TELEFONO → "612345678"
[ ner] ORGANIZACION → "Seguros Mapfre"
[regex] IBAN → "ES91 2100 0418 4502 0005 1332"
Estructura de Archivos
anonimizador/
├── config.py # Configuración, patrones, reglas
├── detector_ner.py # Capa 1: NER (transformers)
├── detector_regex.py # Capa 2: Regex
├── detector_llm.py # Capa 3: LLM (Ollama)
├── anonimizador.py # Motor principal: unifica + reemplaza
└── main.py # Punto de entrada
Personalización de Directrices LLM
Las directrices pueden externalizarse a archivos de texto por dominio:
directrices/
├── medico.txt # directrices para informes médicos
├── judicial.txt # directrices para documentos legales
└── financiero.txt # directrices para documentos bancarios
def cargar_directrices(dominio: str = "medico") -> str:
with open(f"directrices/{dominio}.txt", "r", encoding="utf-8") as f:
return f.read()
Esto permite que un especialista en protección de datos ajuste las reglas sin necesitar un programador.
Plan de Implementación (Roadmap 8 Fases / 47 Pasos)
Contenido integrado desde la nota original "Deanon Steps" (99_Originales/OSINT/Methodology/Deanon steps)
Fase 1: Preparación
- Definir stack tecnológico final (Python + framework GUI)
- Configurar entorno de desarrollo
- Crear repositorio Git
- Definir estructura de carpetas del proyecto
Fase 2: Motor Core
- Implementar extractor de texto (.doc, .docx, .md, .txt)
- Implementar conversor a markdown
- Crear sistema de detección con Regex (emails, IPs, teléfonos, etc.)
- Integrar spaCy para NER en español e inglés
- Implementar generador de tokens (consonantes aleatorias)
- Crear modelo de datos para entidades y relaciones
- Implementar diccionario de mapeo bidireccional
- Crear sistema de cifrado AES-256 para diccionario
Fase 3: Integración LLM Local
- Integrar Ollama como dependencia
- Configurar descarga automática de Phi-3 Mini
- Crear capa de comunicación con la LLM
- Implementar detección de entidades ambiguas con LLM
- Implementar sugerencias de vinculación inteligente
- Crear sistema de fallback (funcionar sin LLM si falla)
Fase 4: Interfaz Gráfica
- Diseñar layout principal (panel texto + árbol identidades)
- Implementar sistema de tabs (Original / Anonimizado)
- Crear árbol jerárquico de identidades con drag & drop
- Implementar menú contextual (click derecho)
- Crear diálogos de confirmación
- Implementar deshacer/rehacer
- Crear modo redacción (toggle)
- Implementar ventana ampliada con búsqueda y zoom
- Crear panel de Prioridades y Exclusiones
- Implementar gestión de proyectos (crear, abrir, guardar)
Fase 5: Historial y Auditoría
- Implementar sistema de versionado (20 versiones)
- Crear vista timeline con reversión
- Implementar log de auditoría
- Crear interfaz de consulta de logs (filtros, búsqueda)
- Implementar detección de modificaciones externas al diccionario
Fase 6: Des-anonimización
- Implementar carga de documento anonimizado
- Crear flujo de selección de diccionario + contraseña
- Implementar motor de des-anonimización
- Crear verificación de coherencia post des-anonimización
Fase 7: Testing y Pulido
- Escribir tests unitarios para motor core
- Escribir tests de integración
- Probar con documentos reales en español e inglés
- Optimizar rendimiento (documentos grandes)
- Pulir interfaz y experiencia de usuario
Fase 8: Empaquetado y Distribución
- Configurar empaquetado multiplataforma (Windows, Mac, Linux)
- Crear instalador con Ollama + Phi-3 Mini incluido
- Escribir documentación de usuario
- Crear guía de instalación
- Publicar en repositorio open source
Notas de Bugs del Proyecto Ralph
- Las entidades y las entidades vinculadas deben mostrarse en el diccionario y en el grafo de manera ordenada y jerárquica
- Las entidades identificadas como IPv4 en el documento son añadidas al diccionario con el código TEL de TELEFONO (error de clasificación)
- Lo mismo ocurre con las entidades MAC al vincularlas: se les asigna el código TEL erróneamente
- Necesidad de revisar toda la documentación relativa a los códigos de anonimización y su asignación correcta
Relaciones
- Proyecto Ralph (anonimizador Tauri/Rust) - Implementación real basada en esta arquitectura
Referencias
- Modelo NER: Davlan/bert-base-multilingual-cased-ner-hrl (Hugging Face)
- LLM: llama3.2:3b via Ollama (https://ollama.com)
- Frameworks: transformers (Hugging Face), torch (PyTorch)
- Concepto de arquitectura multicapa para maximizar cobertura con resiliencia