Удаление PII локально: инструкция с SLM и Artifex для анонимизации | AiManual
AiManual Logo Ai / Manual.
29 Дек 2025 Гайд

Как удалить личные данные из текста локально: кейс-стади с SLM и Artifex

Пошаговый гайд по удалению персональных данных из текстов локально с помощью Small Language Model и библиотеки Artifex. Решение проблемы приватности без облаков

Проблема: почему удаление PII — это не просто поиск-замена

Представьте: вам нужно обработать тысячи отзывов клиентов, медицинских записей или юридических документов для аналитики, но все они содержат персональные данные (PII — Personally Identifiable Information). Имена, телефоны, адреса, номера паспортов, email-адреса — всё это подпадает под GDPR, CCPA и другие регуляции. Отправлять такие данные в облачные API (типа OpenAI или Anthropic) — прямое нарушение. Даже если в договоре есть пункт о конфиденциальности, факт передачи данных третьей стороне создает риски.

Важно: Регулярные выражения и простые правила часто не справляются. Фраза "Иван родился 12.03.1990" может быть обработана, но что делать с "Доктор Петров назначил встречу на пятницу"? Или с частично скрытыми данными вроде "тел. 8-9**-***-12-34"? Нужен контекстуальный анализ.

Решение: локальная Small Language Model + специализированная библиотека

Вместо облачных сервисов используем комбинацию:

  1. Локальная SLM (Small Language Model) — компактная нейросеть, работающая на вашем компьютере без интернета. Отлично подходят модели семейства Phi-3-mini, Qwen2.5-1.5B или Gemma-2-2b. Они достаточно умны для задачи, но требуют мало ресурсов. Если вы уже работали с Ollama для запуска LLM офлайн, то принцип вам знаком.
  2. Библиотека 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  # для управления переменными окружения
💡
Artifex содержит встроенные модели для обнаружения PII, но они могут быть не самыми точными для русского языка или специфичных форматов. Поэтому мы усиливаем её локальной LLM.

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-скрипт, который будет:

  1. Использовать Artifex для первичного обнаружения PII (номера телефонов, email, ИНН и т.д.)
  2. Сложные случаи (имена, контекстуальные упоминания) отправлять в локальную LLM через API Ollama
  3. Заменять найденные 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, ключевой принцип — баланс между приватностью, производительностью и точностью. Этот подход даёт максимальную приватность, иногда в ущерб скорости, но для многих задач эта жертва оправдана.

🚀
Для дальнейшего углубления рекомендую изучить: 1) сравнение инструментов для локального запуска LLM для выбора оптимального бэкенда, 2) дообучение компактных моделей на своих данных для повышения точности.