Проблема: почему удаление PII — это не просто поиск-замена
Представьте: вам нужно обработать тысячи отзывов клиентов, медицинских записей или юридических документов для аналитики, но все они содержат персональные данные (PII — Personally Identifiable Information). Имена, телефоны, адреса, номера паспортов, email-адреса — всё это подпадает под GDPR, CCPA и другие регуляции. Отправлять такие данные в облачные API (типа OpenAI или Anthropic) — прямое нарушение. Даже если в договоре есть пункт о конфиденциальности, факт передачи данных третьей стороне создает риски.
Важно: Регулярные выражения и простые правила часто не справляются. Фраза "Иван родился 12.03.1990" может быть обработана, но что делать с "Доктор Петров назначил встречу на пятницу"? Или с частично скрытыми данными вроде "тел. 8-9**-***-12-34"? Нужен контекстуальный анализ.
Решение: локальная Small Language Model + специализированная библиотека
Вместо облачных сервисов используем комбинацию:
- Локальная SLM (Small Language Model) — компактная нейросеть, работающая на вашем компьютере без интернета. Отлично подходят модели семейства Phi-3-mini, Qwen2.5-1.5B или Gemma-2-2b. Они достаточно умны для задачи, но требуют мало ресурсов. Если вы уже работали с Ollama для запуска LLM офлайн, то принцип вам знаком.
- Библиотека Artifex — Python-библиотека специально для обнаружения и удаления PII. Она использует как правила, так и ML-модели, и может работать в связке с локальной LLM для сложных случаев.
Этот подход даёт полный контроль над данными. Всё обрабатывается в памяти вашего компьютера, никаких внешних запросов. Идеально для юристов, медиков, исследователей и разработчиков, работающих с чувствительной информацией. Как и в случае с локальным RAG для писем, ключ — в оптимизации под ограниченные ресурсы.
Пошаговый план: от установки до работы с папками документов
1 Подготовка окружения и установка Artifex
Создадим виртуальное окружение и установим необходимые библиотеки. Artifex можно установить через pip.
# Создаем виртуальное окружение
python -m venv pii_env
source pii_env/bin/activate # для Windows: pii_env\Scripts\activate
# Устанавливаем Artifex и дополнительные зависимости
pip install artifex
pip install pandas # для работы с таблицами (опционально)
pip install python-dotenv # для управления переменными окружения
2 Запуск локальной SLM через Ollama
Устанавливаем Ollama (инструкции для разных ОС есть в этом подробном гиде) и скачиваем подходящую модель. Для нашей задачи хорошо подходит Phi-3-mini (3.8B параметров) — хороший баланс между качеством и скоростью.
# Устанавливаем Ollama (пример для Linux/Mac)
curl -fsSL https://ollama.com/install.sh | sh
# Скачиваем модель Phi-3-mini
ollama pull phi3:mini
# Проверяем работу модели
ollama run phi3:mini "Привет!"
Модель запущена и готова к запросам через API на localhost:11434.
3 Создаём гибридный скрипт для анонимизации
Теперь напишем Python-скрипт, который будет:
- Использовать Artifex для первичного обнаружения PII (номера телефонов, email, ИНН и т.д.)
- Сложные случаи (имена, контекстуальные упоминания) отправлять в локальную LLM через API Ollama
- Заменять найденные PII на псевдонимы или метки (например, [ИМЯ_1], [ТЕЛЕФОН_2])
import json
import requests
from artifex import Artifex
from typing import List, Dict
import re
class LocalPIIAnonymizer:
def __init__(self, ollama_url="http://localhost:11434/api/generate"):
"""Инициализация анонимизатора"""
self.artifex = Artifex()
self.ollama_url = ollama_url
self.entity_counter = {}
def detect_with_artifex(self, text: str) -> List[Dict]:
"""Обнаружение PII с помощью Artifex"""
try:
results = self.artifex.process(text)
# Artifex возвращает список найденных сущностей
entities = []
for entity in results.get('entities', []):
entities.append({
'text': entity['text'],
'type': entity['type'],
'start': entity['start'],
'end': entity['end']
})
return entities
except Exception as e:
print(f"Ошибка Artifex: {e}")
return []
def ask_llm_for_contextual_pii(self, text: str) -> List[Dict]:
"""Используем локальную LLM для сложных случаев"""
prompt = f"""Текст: "{text}"
Найди все персональные данные (PII) в тексте выше. Включи:
- Имена и фамилии людей
- Названия организаций, если они могут идентифицировать человека
- Должности в сочетании с именами
- Любые другие данные, которые могут идентифицировать человека
Верни ответ в формате JSON списка объектов с полями: 'text', 'type', 'start_index', 'end_index'.
Пример: [{{"text": "Иван Иванов", "type": "PERSON", "start_index": 0, "end_index": 12}}]
Только JSON, без дополнительного текста."""
payload = {
"model": "phi3:mini",
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.1, # Низкая температура для более детерминированных ответов
"num_predict": 500
}
}
try:
response = requests.post(self.ollama_url, json=payload, timeout=30)
response.raise_for_status()
result = response.json()
llm_output = result.get('response', '').strip()
# Извлекаем JSON из ответа LLM
json_match = re.search(r'\[.*\]', llm_output, re.DOTALL)
if json_match:
entities = json.loads(json_match.group())
return entities
else:
print(f"LLM не вернул валидный JSON: {llm_output[:200]}")
return []
except Exception as e:
print(f"Ошибка при запросе к LLM: {e}")
return []
def anonymize_text(self, text: str) -> str:
"""Основная функция анонимизации"""
# Шаг 1: Обнаружение PII через Artifex
artifex_entities = self.detect_with_artifex(text)
# Шаг 2: Дополнительный контекстуальный анализ через LLM
llm_entities = self.ask_llm_for_contextual_pii(text)
# Шаг 3: Объединяем результаты (убираем дубликаты)
all_entities = artifex_entities.copy()
for llm_ent in llm_entities:
# Проверяем, не перекрывается ли эта сущность с уже найденными
overlap = False
for existing in all_entities:
if (llm_ent['start_index'] >= existing['start'] and
llm_ent['start_index'] <= existing['end']):
overlap = True
break
if not overlap:
all_entities.append({
'text': llm_ent['text'],
'type': llm_ent['type'],
'start': llm_ent['start_index'],
'end': llm_ent['end_index']
})
# Шаг 4: Сортируем по позиции в тексте (с конца к началу)
all_entities.sort(key=lambda x: x['start'], reverse=True)
# Шаг 5: Заменяем PII на анонимные метки
anonymized_text = text
for entity in all_entities:
entity_type = entity['type']
if entity_type not in self.entity_counter:
self.entity_counter[entity_type] = 1
else:
self.entity_counter[entity_type] += 1
replacement = f"[{entity_type}_{self.entity_counter[entity_type]}]"
# Заменяем в тексте
anonymized_text = (
anonymized_text[:entity['start']] +
replacement +
anonymized_text[entity['end']:]
)
return anonymized_text
# Пример использования
if __name__ == "__main__":
anonymizer = LocalPIIAnonymizer()
sensitive_text = """
Пациент: Иван Сидоров, тел. +7 (912) 345-67-89.
Адрес: г. Москва, ул. Ленина, д. 10, кв. 5.
Диагноз установила врач высшей категории Петрова Мария Сергеевна.
Рекомендовано наблюдение у кардиолога в клинике 'Здоровье'.
Email для связи: ivan.sidorov@mail.ru
"""
clean_text = anonymizer.anonymize_text(sensitive_text)
print("Очищенный текст:")
print(clean_text)
4 Обработка файлов и папок
Для работы с большими объемами данных добавим обработку файлов разных форматов:
import os
from pathlib import Path
def process_file(file_path: str, anonymizer: LocalPIIAnonymizer) -> str:
"""Обработка одного файла"""
ext = Path(file_path).suffix.lower()
try:
if ext == '.txt':
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
elif ext == '.json':
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Предполагаем, что нужный текст в поле 'content'
content = data.get('content', '')
else:
print(f"Формат {ext} не поддерживается, пропускаем")
return ""
anonymized = anonymizer.anonymize_text(content)
# Сохраняем результат
output_path = file_path.replace(ext, f'_cleaned{ext}')
with open(output_path, 'w', encoding='utf-8') as f:
f.write(anonymized)
print(f"Обработан: {file_path} -> {output_path}")
return anonymized
except Exception as e:
print(f"Ошибка при обработке {file_path}: {e}")
return ""
def process_folder(folder_path: str):
"""Обработка всех текстовых файлов в папке"""
anonymizer = LocalPIIAnonymizer()
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.endswith(('.txt', '.json')):
file_path = os.path.join(root, file)
process_file(file_path, anonymizer)
print("Обработка завершена!")
# Запуск обработки папки
# process_folder("./documents/")
Нюансы и возможные ошибки
| Проблема | Решение | Причина |
|---|---|---|
| LLM возвращает не JSON, а текст | Добавить пост-обработку с regex для извлечения JSON, использовать более строгие инструкции в промпте | Модель может "додумывать" ответ, особенно при высокой температуре |
| Медленная обработка больших текстов | Разбивать текст на чанки по 500-1000 символов, использовать кэширование | LLM имеет ограничения на длину контекста, большие запросы медленные |
| Пропуск некоторых PII | Комбинировать несколько методов: Artifex + LLM + собственные regex-правила | Ни один метод не идеален, нужна эшелонированная защита |
| Высокая загрузка CPU/GPU | Использовать более легкие модели (Phi-2, TinyLlama), ограничивать параллельные запросы | LLM инференс ресурсоемкий, особенно на CPU |
FAQ: ответы на частые вопросы
Вопрос: Насколько надёжно это решение по сравнению с коммерческими облачными сервисами?
Ответ: С точки зрения приватности — абсолютно надёжно, данные никуда не уходят. С точки зрения точности — коммерческие сервисы могут использовать более крупные модели (как в статье о приватности ChatGPT), но для большинства задач SLM достаточно. Точность можно повысить, дообучив модель на своих данных.
Вопрос: Можно ли использовать этот подход для реального продакшена?
Ответ: Да, но с оптимизациями: 1) Заменить Ollama на более производительный бэкенд (llama.cpp напрямую), 2) Добавить батчинг запросов, 3) Настроить кэширование результатов. Для высоконагруженных систем стоит рассмотреть выделенный сервер с GPU.
Вопрос: Как обрабатывать документы в форматах PDF, DOCX?
Ответ: Добавьте этап конвертации в текст с помощью библиотек типа pypdf (для PDF) или python-docx (для DOCX). Примерно так же, как в гайде по обработке документов в Obsidian.
Заключение: когда это действительно нужно
Локальная анонимизация PII с помощью SLM и Artifex — не универсальное решение, а специализированный инструмент для конкретных сценариев:
- Юридические и медицинские организации, которые физически не могут отправлять данные в облако
- Исследователи, работающие с чувствительными данными перед публикацией
- Разработчики, создающие системы обработки пользовательского контента в странах с жёстким регулированием
- Компании, которые хотят полностью контролировать цепочку обработки данных
Как и при работе с идеальным стеком для self-hosted LLM, ключевой принцип — баланс между приватностью, производительностью и точностью. Этот подход даёт максимальную приватность, иногда в ущерб скорости, но для многих задач эта жертва оправдана.