Sélectionner une page
Accueil » Tous les articles » Tester la pseudonymisation de documents chez soi : anonymiser, traiter par IA, réinjecter

Tester la pseudonymisation de documents chez soi : anonymiser, traiter par IA, réinjecter

par | Juil 2, 2026 | IA & Automatisation | 0 commentaires

| Mis à jour le 2 juillet 2026

Cet article fait partie de la série des articles sur la création d’une IA locale dans mon bureau : Créer une IA locale.

Anonymiser un document, c’est supprimer définitivement les données personnelles qu’il contient. Pseudonymiser, c’est différent : tu remplaces temporairement ces données par un identifiant neutre, tu traites le document sans jamais voir l’information sensible, puis tu réinjectes les vraies données à la fin. Voici comment tester ce principe chez toi, sur un PC modeste, avec des outils open source.

Le principe en 3 étapes

  1. Détecter les données personnelles dans un texte et les remplacer par un jeton neutre (par exemple PERSONNE_1 à la place d’un prénom).
  2. Traiter le texte anonymisé avec une intelligence artificielle, qui ne voit jamais l’identité réelle de la personne.
  3. Réinjecter l’information réelle à la place du jeton dans le résultat final.

L’intérêt : le traitement (recherche, analyse, génération de contenu) se fait sans exposer l’identité de la personne concernée, ce qui limite les risques si le document venait à être mal utilisé ou transmis à un tiers.

Les outils utilisés

  • Presidio, un outil open source de Microsoft, pour détecter les données personnelles dans le texte (reconnaissance d’entités nommées).
  • spaCy, une bibliothèque de traitement du langage, utilisée par Presidio pour comprendre le texte en français.
  • Ollama avec le modèle qwen2.5:3b, pour le traitement IA local, sans aucune donnée envoyée sur internet.

Tout tourne en local sur un Mini PC Linux, sans connexion à un service externe.

Installer l’environnement

Le script tourne dans un environnement Python isolé (un venv), séparé du reste du système. Ça évite que les bibliothèques installées pour ce projet entrent en conflit avec d’autres outils déjà présents sur la machine.

Le projet est placé dans un dossier home/USER/docker/ qui est sauvegardé chaque nuit (voir Sauvegarder ses containers Docker automatiquement avec Rclone).

Crée le dossier du projet et vérifie que le paquet nécessaire à la création d’un venv est installé :

mkdir -p ~/docker/pseudonymisation
cd ~/docker/pseudonymisation
dpkg -l | grep python3-venv

Si la commande ne retourne rien, installe le paquet (le numéro de version dépend de ta machine, la commande d’installation exacte apparaît dans le message d’erreur si tu tentes de créer le venv directement) :

sudo apt update
sudo apt upgrade
sudo reboot

Après le redémarrage, reconnecte-toi en SSH et installe le paquet indiqué par le message d’erreur, par exemple :

sudo apt install python3.12-venv

Crée ensuite le venv, sans sudo (une commande avec sudo donnerait les droits root aux fichiers créés, ce qui complique ensuite toute modification) :

cd ~/docker/pseudonymisation
python3 -m venv venv
source venv/bin/activate

La ligne de commande affiche maintenant (venv) au début, signe que l’environnement isolé est actif. Installe les bibliothèques nécessaires et le modèle de langue française :

pip install presidio-analyzer spacy
python -m spacy download fr_core_news_sm

Le modèle fr_core_news_sm est le plus léger disponible pour le français. Sur un PC peu puissant et pour un test avec des textes courts, il est peut-être suffisant. Un modèle plus complet (fr_core_news_md) existe si la détection s’avère insuffisante.

Enregistre la liste des bibliothèques installées, pour pouvoir tout réinstaller à l’identique en cas de besoin :

pip freeze > requirements.txt

Vérifier que la détection fonctionne

Avant de construire le script complet, un test simple permet de vérifier que Presidio détecte bien un prénom dans une phrase.

Crée le fichier test_detection.py :

from presidio_analyzer import AnalyzerEngine
from presidio_analyzer.nlp_engine import NlpEngineProvider

configuration = {
    "nlp_engine_name": "spacy",
    "models": [{"lang_code": "fr", "model_name": "fr_core_news_sm"}]
}
provider = NlpEngineProvider(nlp_configuration=configuration)
nlp_engine = provider.create_engine()
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=["fr"])

texte_origine = "Bonjour, je m'appelle Pierre Durand et j'habite à Paris."

resultats = analyzer.analyze(text=texte_origine, language="fr")

for res in resultats:
    print(f"Trouvé: {texte_origine[res.start:res.end]} -> Catégorie: {res.entity_type}")

Lance-le :

python test_detection.py

Résultat obtenu :

Trouvé: Pierre Durand -> Catégorie: PERSON
Trouvé: Paris -> Catégorie: LOCATION

La détection fonctionne, on peut construire le script complet.

Créer des documents de test

Pour tester le workflow, j’ai créé 5 courts textes fictifs, chacun avec un prénom, un âge et un animal préféré, rédigés comme des présentations naturelles plutôt que des formulaires. Crée un dossier documents/ et un fichier par personne :

mkdir -p ~/docker/pseudonymisation/documents
cat > ~/docker/pseudonymisation/documents/personne1.txt << 'EOF'
Bonjour, je m'appelle Léa et j'ai 34 ans. Je travaille dans une pépinière depuis quelques années. Si je devais choisir un animal préféré, ce serait sans hésiter l'axolotl, je trouve sa capacité à régénérer ses membres absolument fascinante.
EOF
cat > ~/docker/pseudonymisation/documents/personne2.txt << 'EOF'
Je me présente, je suis Thomas, j'ai 52 ans et je vis à la campagne. Mon animal préféré est le fennec, ses grandes oreilles et son adaptation au désert m'ont toujours impressionné depuis un documentaire vu il y a longtemps.
EOF
cat > ~/docker/pseudonymisation/documents/personne3.txt << 'EOF'
Salut, moi c'est Camille, 27 ans. J'adore les animaux un peu décalés, et mon préféré reste le quokka, ce petit marsupial australien qui semble toujours sourire sur les photos.
EOF
cat > ~/docker/pseudonymisation/documents/personne4.txt << 'EOF'
Je m'appelle Julien et j'ai 45 ans. Passionné de nature depuis l'enfance, mon animal préféré est le pangolin, une créature discrète et méconnue que je trouve pourtant étonnante avec ses écailles.
EOF
cat > ~/docker/pseudonymisation/documents/personne5.txt << 'EOF'
Bonjour, je suis Sophie, j'ai 61 ans et je suis récemment partie à la retraite. Mon animal préféré est l'okapi, cet étrange cousin de la girafe que j'ai découvert lors d'un voyage il y a quelques années.
EOF

Le script d’anonymisation

Ce script lit chaque fichier, détecte les entités présentes (pas seulement le prénom, pour observer aussi ce que Presidio détecte d’autre), les remplace par des jetons, et sauvegarde la correspondance dans un fichier JSON.

Crée anonymisation.py :

import os
import json
from presidio_analyzer import AnalyzerEngine
from presidio_analyzer.nlp_engine import NlpEngineProvider

DOSSIER_SOURCE = "documents"
DOSSIER_ANONYMISE = "documents_anonymises"
DOSSIER_RECONSTRUIT = "documents_reconstruits"
FICHIER_CORRESPONDANCE = "correspondance.json"

configuration = {
    "nlp_engine_name": "spacy",
    "models": [{"lang_code": "fr", "model_name": "fr_core_news_sm"}]
}
provider = NlpEngineProvider(nlp_configuration=configuration)
nlp_engine = provider.create_engine()
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=["fr"])

os.makedirs(DOSSIER_ANONYMISE, exist_ok=True)
os.makedirs(DOSSIER_RECONSTRUIT, exist_ok=True)

correspondance_globale = {}

for nom_fichier in sorted(os.listdir(DOSSIER_SOURCE)):
    if not nom_fichier.endswith(".txt"):
        continue

    chemin_source = os.path.join(DOSSIER_SOURCE, nom_fichier)
    with open(chemin_source, "r", encoding="utf-8") as f:
        texte = f.read()

    resultats = analyzer.analyze(text=texte, language="fr")
    resultats_tries = sorted(resultats, key=lambda r: r.start, reverse=True)

    correspondance_fichier = {"prenom": None, "autres_elements_personnels": []}
    texte_anonymise = texte
    compteur_autres = 0
    prenom_trouve = False

    for res in resultats_tries:
        valeur = texte[res.start:res.end]

        if res.entity_type == "PERSON" and not prenom_trouve:
            jeton = "PERSONNE_1"
            correspondance_fichier["prenom"] = {"jeton": jeton, "valeur": valeur}
            prenom_trouve = True
        else:
            compteur_autres += 1
            jeton = f"AUTRE_{compteur_autres}"
            correspondance_fichier["autres_elements_personnels"].append(
                {"jeton": jeton, "valeur": valeur, "categorie": res.entity_type}
            )

        texte_anonymise = texte_anonymise[:res.start] + jeton + texte_anonymise[res.end:]

    with open(os.path.join(DOSSIER_ANONYMISE, nom_fichier), "w", encoding="utf-8") as f:
        f.write(texte_anonymise)

    correspondance_globale[nom_fichier] = correspondance_fichier

    texte_reconstruit = texte_anonymise
    if correspondance_fichier["prenom"]:
        texte_reconstruit = texte_reconstruit.replace(
            correspondance_fichier["prenom"]["jeton"],
            "[ " + correspondance_fichier["prenom"]["valeur"] + " ]"
        )

    if correspondance_fichier["autres_elements_personnels"]:
        texte_reconstruit += "\n\nAutres elements personnels detectes :\n"
        for item in correspondance_fichier["autres_elements_personnels"]:
            texte_reconstruit += f"- {item['jeton']} : {item['valeur']} ({item['categorie']})\n"

    with open(os.path.join(DOSSIER_RECONSTRUIT, nom_fichier), "w", encoding="utf-8") as f:
        f.write(texte_reconstruit)

with open(FICHIER_CORRESPONDANCE, "w", encoding="utf-8") as f:
    json.dump(correspondance_globale, f, ensure_ascii=False, indent=2)

print("Termine. Verifie documents_anonymises/, documents_reconstruits/ et correspondance.json")

Lance le script :

python anonymisation.py

Le dossier documents_reconstruits/ sert de test intermédiaire : il réinjecte immédiatement le prénom entre crochets ([ Léa ]) pour vérifier visuellement que le mécanisme fonctionne, avant même d’ajouter le traitement IA.

Sur les 5 documents testés, la détection a globalement bien fonctionné, avec deux limites observées :

  • sur personne1.txt, Presidio n’a détecté aucune entité, y compris le prénom Léa pourtant présent en clair. Le modèle léger fr_core_news_sm peut manquer certains prénoms.
  • sur personne3.txt, le mot « Salut » a été classé comme un lieu (LOCATION), un faux positif.

Ces limites sont attendues avec un modèle allégé et font partie de ce qu’on cherche à observer dans ce test.

Le traitement par IA

Le texte anonymisé est envoyé à qwen2.5:3b via Ollama, avec une consigne qui demande à la fois l’animal préféré et un signalement de toute donnée personnelle restante dans le texte, une façon de vérifier si l’IA repère les éventuels oublis de l’étape précédente.

Un test sur un seul fichier permet de vérifier le format de réponse avant de généraliser :

import requests

with open("documents_anonymises/personne2.txt", "r", encoding="utf-8") as f:
    texte_anonymise = f.read()

prompt = f"""Voici un texte. Reponds uniquement avec ce format exact, sans phrase supplementaire :
Animal prefere : [ton animal trouve]
Attention - donnees personnelles : [liste les elements qui sont des donnees personnelles, ou ecris "aucun"]

Texte : {texte_anonymise}"""

reponse = requests.post(
    "http://localhost:11434/api/generate",
    json={"model": "qwen2.5:3b", "prompt": prompt, "stream": False}
)

print(reponse.json()["response"])

Une fois le format validé, le script complet traite les 5 documents :

import os
import json
import requests

DOSSIER_ANONYMISE = "documents_anonymises"
FICHIER_RESULTATS = "resultats_ia.json"

resultats = {}

for nom_fichier in sorted(os.listdir(DOSSIER_ANONYMISE)):
    if not nom_fichier.endswith(".txt"):
        continue

    chemin = os.path.join(DOSSIER_ANONYMISE, nom_fichier)
    with open(chemin, "r", encoding="utf-8") as f:
        texte_anonymise = f.read()

    prompt = f"""Voici un texte. Reponds uniquement avec ce format exact, sans phrase supplementaire :
Animal prefere : [ton animal trouve]
Attention - donnees personnelles : [liste les elements qui sont des donnees personnelles, ou ecris "aucun"]

Texte : {texte_anonymise}"""

    reponse = requests.post(
        "http://localhost:11434/api/generate",
        json={"model": "qwen2.5:3b", "prompt": prompt, "stream": False}
    )

    resultats[nom_fichier] = reponse.json()["response"]
    print(f"{nom_fichier} traite.")

with open(FICHIER_RESULTATS, "w", encoding="utf-8") as f:
    json.dump(resultats, f, ensure_ascii=False, indent=2)

print("Termine. Verifie resultats_ia.json")

Sur plusieurs exécutions successives de ce script, un même document (personne5.txt) a systématiquement échoué à respecter le format demandé : au lieu de répondre selon la consigne, le modèle a recopié le texte source. Un autre document a échoué une fois sur trois essais, avant de fonctionner correctement. Ce comportement n’est pas lié au prompt (qui fonctionne pour la majorité des textes), plutôt à une limite connue des petits modèles sur le respect strict d’un format de sortie.

La réinjection finale

Le dernier script combine le prénom réel, l’animal trouvé par l’IA et les éventuelles autres données personnelles détectées, pour produire un document final par personne :

import os
import json

FICHIER_CORRESPONDANCE = "correspondance.json"
FICHIER_RESULTATS_IA = "resultats_ia.json"
DOSSIER_FINAL = "documents_finaux"

with open(FICHIER_CORRESPONDANCE, "r", encoding="utf-8") as f:
    correspondance = json.load(f)

with open(FICHIER_RESULTATS_IA, "r", encoding="utf-8") as f:
    resultats_ia = json.load(f)

os.makedirs(DOSSIER_FINAL, exist_ok=True)

for nom_fichier, corr in correspondance.items():
    prenom = corr["prenom"]["valeur"] if corr["prenom"] else "PRENOM_NON_DETECTE"

    reponse_ia = resultats_ia.get(nom_fichier, "")
    animal = "non determine (echec du modele IA)"
    for ligne in reponse_ia.splitlines():
        if ligne.strip().lower().startswith("animal prefere"):
            animal = ligne.split(":", 1)[1].strip()
            break

    contenu_final = f"Prenom : {prenom}\n"
    contenu_final += f"Animal prefere : {animal}\n"

    if corr["autres_elements_personnels"]:
        contenu_final += "\nAutres elements personnels detectes :\n"
        for item in corr["autres_elements_personnels"]:
            contenu_final += f"- {item['jeton']} : {item['valeur']} ({item['categorie']})\n"

    nom_sortie = nom_fichier.replace(".txt", "_final.txt")
    with open(os.path.join(DOSSIER_FINAL, nom_sortie), "w", encoding="utf-8") as f:
        f.write(contenu_final)

print("Termine. Verifie le dossier documents_finaux/")

Résultat obtenu sur les 5 documents :

Prenom : PRENOM_NON_DETECTE
Animal prefere : axolotl

Prenom : Thomas
Animal prefere : fennec

Prenom : Camille
Animal prefere : Quokka
Autres elements personnels detectes :
- AUTRE_1 : Salut (LOCATION)

Prenom : Julien
Animal prefere : pangolin

Prenom : Sophie
Animal prefere : non determine (echec du modele IA)

Chaque cas d’échec est signalé explicitement dans le document final, plutôt que masqué ou laissé vide, ce qui permet de voir immédiatement où le workflow a besoin d’être amélioré.

Ce qu’on a appris

  • Un PC ancien de bureautique suffit. Aucune lenteur notable, y compris pendant les 5 appels au modèle IA.
  • Le modèle spaCy léger a ses limites. Un prénom sur cinq n’a pas été détecté, et un faux positif est apparu sur un autre texte. Un modèle plus complet (fr_core_news_md) mériterait un test comparatif avant un usage réel.
  • Le petit modèle IA est inconstant sur le format. Un même texte peut réussir ou échouer à respecter la consigne selon l’essai. Une piste pour réduire cette variabilité, non testée ici : fixer le paramètre temperature à 0 dans la requête envoyée à Ollama.
  • Le principe fonctionne malgré ces limites. Le workflow complet, de la détection à la réinjection, tourne de bout en bout sans erreur bloquante, avec une gestion propre des cas d’échec.

Des cas d’usage possibles en entreprise

Ce principe dépasse largement le simple test avec des animaux préférés. Quelques exemples concrets :

  • Formulaires de contact ou de prospection : un prospect décrit son besoin, le texte est anonymisé avant d’être traité par une IA qui propose une base de réponse ou de devis, puis l’identité est réinjectée à la fin.
  • Formation : traiter des retours d’expérience ou des évaluations sans exposer l’identité des participants pendant l’analyse.
  • Plus largement, tout traitement IA sur des documents contenant des données sensibles, où l’on veut garder un contrôle strict sur qui voit quoi et à quel moment.

0 0 votes
Évaluation de l'article
0
Nous aimerions avoir votre avis, veuillez laisser un commentaire.x