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) :
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 :
É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.