1 juil. 2025

Extraire des données de documents avec un LLM local (Mistral + Ollama) : une alternative économique à l'API OpenAI

Data & IA

Portrait collaborateur.

Terry

Malik

1 juil. 2025

Extraire des données de documents avec un LLM local (Mistral + Ollama) : une alternative économique à l'API OpenAI

Data & IA

Portrait collaborateur.

Terry

Malik

1 juil. 2025

Extraire des données de documents avec un LLM local (Mistral + Ollama) : une alternative économique à l'API OpenAI

Data & IA

Portrait collaborateur.

Terry

Malik

Ce que vous allez retenir

  • Comment configurer un environnement Python pour traiter automatiquement des documents Word et PDF.

  • Utiliser regex pour extraire rapidement les informations simples (email, téléphone, etc.).

  • Exploiter un modèle LLM local (Mistral avec Ollama) pour analyser des contenus complexes sans API externe.

  • Mettre en place une stratégie combinée (regex + LLM) pour équilibrer vitesse, précision et coût.

  • Générer et enrichir un fichier Excel avec les données extraites de façon sécurisée et autonome.

Introduction

L’essor des grands modèles de langage (LLMs) a révolutionné le traitement des documents. Pourtant, beaucoup de tutoriels reposent sur l'API d'OpenAI ou d'autres solutions cloud payantes. Dans cet article, nous allons voir comment utiliser un modèle local comme Mistral grâce à Ollama pour extraire intelligemment des données depuis des documents Word ou PDF, le tout sans consommer de tokens et sans exposer vos données à des serveurs distants.

Nous verrons aussi pourquoi combiner des regex rapides pour l’extraction “évidente” et un LLM pour l’analyse plus fine est une stratégie gagnante.


Étape 1 : Préparer votre environnement Python

Avant tout, il vous faut :

  • Python 3.10+

  • Ollama installé localement (documentation officielle)

  • Le modèle Mistral téléchargé localement via Ollama

  • Les bibliothèques Python suivantes : python-docx, PyPDF2, re, pandas, openpyxl, tqdm, etc.


Installation de Python :

Pour Windows, téléchargez depuis : https://www.python.org/downloads/windows

Installez le .exe et SURTOUT, cochez absolument la case “Add Python to PATH” avant de cliquer sur “Install Now”.

Vous pouvez ensuite installer les dépendances via pip (dans l'invite de commande Win+R, aussi appelée "terminal" ou "shell") :

pip install python-docx PyPDF2 pandas openpyxl tqdm

Avant de pouvoir utiliser Mistral, vous devez d’abord télécharger le modèle avec la commande suivante dans votre terminal (cmd, PowerShell ou terminal VSCode par exemple) :

ollama pull mistral

Cela va récupérer le modèle Mistral 7B depuis le dépôt officiel d’Ollama. L’opération peut prendre plusieurs minutes selon votre connexion (environ 4 Go à télécharger).

Une fois téléchargé, vous pouvez démarrer le modèle localement en exécutant :

ollama run mistral


Étape 2 : Lire des fichiers Word et PDF

Le cœur de votre script repose sur la capacité à extraire du texte brut depuis différents formats :

from PyPDF2 import PdfReader
from docx import Document

def extract_text_from_pdf(file_path):
    text = ""
    reader = PdfReader(file_path)
    for page in reader.pages:
        text += page.extract_text() or ""
    return text

def extract_text_from_docx(file_path):
    doc = Document(file_path)
    return "\n".join([para.text for para in doc.paragraphs])

Vous pourrez ensuite appliquer une fonction générique :

def extract_text(file_path):
    try:
        if file_path.endswith(".pdf"):
            return extract_text_from_pdf(file_path)
        elif file_path.endswith(".docx"):
            return extract_text_from_docx(file_path)
    except Exception as e:
        print(f"[Erreur] Impossible de lire le fichier '{file_path}' : {e}")
    return ""


Étape 3 : Extraire rapidement les données évidentes (regex)

Utiliser un LLM pour toutes les extractions serait lent et inutile. Vous pouvez commencer par des regex pour capter les données standardisées comme :

  • Prénom

  • Nom

  • Email

  • Téléphone

  • URL Linkedin

  • Code postal

Exemple :

import re

def extract_email(text):
    # Regex avec contrôle de l'extension du domaine (2 à 10 lettres uniquement)
    match = re.search(r"[\w\.-]+@[\w\.-]+\.(?:[a-zA-Z]{2,10})\b", text)
    return match.group(0) if match else ""

def extract_phone(text):
    match = re.search(r"(?<!\d)(?:\+33|0)[1-9](?:[ .-]?\d{2}){4}(?!\d)", text)
    return match.group(0) if match else ""

def extract_postal_code(text):
    match = re.search(r"\b\d{5}\b", text)
    return match.group(0) if match else ""

def extract_linkedin(text):
    match = re.search(r"https?://(www\.)?linkedin\.com/in/[a-zA-Z0-9_-]+", text)
    return match.group(0) if match else ""

def extract_name(text):
    match = re.search(r"Nom\s*[:\-]?\s*([A-Z][a-z]+)", text)
    return match.group(1) if match else ""

def extract_firstname(text):
    match = re.search(r"Pr[ée]nom\s*[:\-]?\s*([A-Z][a-z]+)", text)
    return match.group(1) if match else ""

Cela permet d’éviter de solliciter le LLM inutilement quand l’information est clairement identifiable.


Étape 4 : Compléter avec un LLM local pour les données implicites

Une fois les informations "faciles" extraites, on peut interroger Mistral via Ollama pour déduire les éléments manquants ou interpréter du texte flou.

Attention, il faut un ordinateur suffisamment puissant, au minimum avec un CPU moderne ou, idéalement, une carte graphique avec 6 à 8 Go de VRAM (ex : RTX 3060 ou équivalent) pour une exécution fluide.

import subprocess

def query_mistral(prompt, system_message="Tu es un assistant d'extraction d'information. Réponds en JSON."):
    command = ['ollama', 'run', 'mistral']
    full_prompt = f"<s>[INST] {system_message}\n{prompt} [/INST]"
    result = subprocess.run(command, input=full_prompt.encode(), stdout=subprocess.PIPE)
    return result.stdout.decode()


Exemple de prompt, n'hésitez pas à l'adapter à vos attentes :

prompt = f"""Voici un texte extrait d’un document :
{text}

Peux-tu identifier les informations suivantes s’il y en a :
- Nom
- Prénom
- Diplômes
- Compétences techniques
- Langues parlées
- Localisation géographique
Réponds en JSON compact.
"""


Étape 5 : Sauvegarder et enrichir les résultats

Enfin, vous pouvez enregistrer les résultats dans un fichier Excel avec pandas, en prenant soin de faire une sauvegarde incrémentale à chaque exécution pour éviter les pertes de données.

import pandas as pd
import os

def save_results_to_excel(data_dict, output_file="resultats.xlsx"):
    df_new = pd.DataFrame([data_dict])

    if os.path.exists(output_file):
        df_existing = pd.read_excel(output_file)
        df_combined = pd.concat([df_existing, df_new], ignore_index=True)
    else:
        df_combined = df_new

    df_combined.to_excel(output_file, index=False)


Pourquoi utiliser un LLM local ?

  • Pas de token, pas de coût : contrairement à l’API OpenAI, vous pouvez exécuter autant de requêtes que vous voulez sans frais.

  • Protection de la vie privée : vos documents restent sur votre machine.

  • Résilience : plus besoin de connexion internet ou de quota d’API.

Cela rend cette approche idéale pour des applications internes et/ou ponctuelles, industrielles ou sensibles.


Astuce : combiner vitesse et intelligence

La bonne pratique consiste à ne pas tout déléguer au LLM. En combinant des regex rapides pour les cas simples et un LLM local pour l’interprétation complexe, vous obtenez le meilleur des deux mondes :

  • Rapidité

  • Précision

  • Coût maîtrisé


Conclusion

Vous avez désormais une base pour construire un scraper de documents intelligent et économique, sans dépendre d’un service externe.

Que ce soit pour traiter des CV, des fiches projets ou des rapports internes, cette méthode vous permet de tirer parti des LLMs en autonomie complète.

Voici une version complète du code, prête à utiliser. Vous pouvez l'adapter à vos propres besoins : changer les règles d'extraction, enrichir le prompt, ou rediriger les résultats vers une base de données par exemple.

import os
import re
import docx
import PyPDF2
import pandas as pd
import subprocess
from pathlib import Path
from datetime import datetime

# 📄 Lecture de fichiers Word et PDF avec gestion d'erreur
def extract_text_from_docx(file_path):
    try:
        doc = docx.Document(file_path)
        return "\n".join(p.text for p in doc.paragraphs)
    except Exception as e:
        print(f"[Erreur DOCX] {file_path} : {e}")
        return ""

def extract_text_from_pdf(file_path):
    text = ""
    try:
        with open(file_path, "rb") as f:
            reader = PyPDF2.PdfReader(f)
            for page in reader.pages:
                content = page.extract_text()
                if content:
                    text += content + "\n"
        return text
    except Exception as e:
        print(f"[Erreur PDF] {file_path} : {e}")
        return ""

# 🔎 Extraction rapide avec regex robustes
def extract_info(text):
    info = {}

    # Email avec contrôle d'extension (2 à 10 lettres)
    email_match = re.search(r'[\w\.-]+@[\w\.-]+\.[a-zA-Z]{2,10}\b', text)
    info["email"] = email_match.group(0) if email_match else ""

    # Téléphone français uniquement : +33 ou 0 suivi de 9 chiffres
    phone_match = re.search(r'(?<!\d)(?:\+33|0)[1-9](?:[ .-]?\d{2}){4}(?!\d)', text)
    info["phone"] = phone_match.group(0) if phone_match else ""

    # Profil LinkedIn
    linkedin_match = re.search(r'https?://(www\.)?linkedin\.com/in/[^\s>]+', text)
    info["linkedin"] = linkedin_match.group(0) if linkedin_match else ""

    # Déduction prénom / nom depuis l'email
    if info["email"]:
        parts = re.split(r'[._\-]', info["email"].split("@")[0])
        info["first_name"] = parts[0].capitalize() if len(parts) > 0 else ""
        info["last_name"] = parts[1].capitalize() if len(parts) > 1 else ""
    else:
        info["first_name"] = ""
        info["last_name"] = ""

    return info

# 🤖 Interroger Mistral via Ollama
def query_mistral(prompt):
    try:
        result = subprocess.run(
            ["ollama", "run", "mistral"],
            input=prompt.encode("utf-8"),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=30
        )
        return result.stdout.decode("utf-8", errors="ignore")
    except Exception as e:
        return f"Erreur LLM : {e}"

# 💾 Sauvegarde dans Excel avec ajout incrémental
def save_results(data, output_file="résultats.xlsx"):
    if os.path.exists(output_file):
        df_existing = pd.read_excel(output_file)
        df = pd.concat([df_existing, pd.DataFrame([data])], ignore_index=True)
    else:
        df = pd.DataFrame([data])
    df.to_excel(output_file, index=False)

# 🧪 Traitement de tous les fichiers d’un dossier
def process_documents(folder_path):
    folder = Path(folder_path)
    for file in folder.glob("*"):
        if file.suffix.lower() in [".pdf", ".docx"]:
            print(f"📂 Traitement : {file.name}")
            text = extract_text_from_pdf(file) if file.suffix == ".pdf" else extract_text_from_docx(file)

            if not text.strip():
                print(f"⚠️ Texte vide ou fichier illisible : {file.name}")
                continue

            info = extract_info(text)

            if not info["first_name"] or not info["last_name"]:
                prompt = f"Voici un extrait de CV :\n{text[:1500]}\n\nPeux-tu en déduire le prénom, le nom et la profession ?"
                info["llm_response"] = query_mistral(prompt).strip()
            else:
                info["llm_response"] = ""

            info["file_name"] = file.name
            info["timestamp"] = datetime.now().isoformat()
            save_results(info)

    print("✅ Tous les documents ont été traités.")

# 🚀 Exécution principale
if __name__ == "__main__":
    default_folder = "./cv"
    os.makedirs(default_folder, exist_ok=True)
    print(f"📁 Dossier à traiter : {default_folder}")
    process_documents(default_folder)

Enfin, mettez vous dans le répertoire du script, clic-droit et ouvrez votre terminal.
Exécutez votre script.

python extract.py