Que fait Regex vs LLM pour l'extraction de texte structuré ?
Un cadre de décision pratique pour analyser du texte structuré (quiz, formulaires, factures, documents). L'idée clé : les regex traitent 95 à 98 % des cas de manière économique et déterministe. Réservez les appels LLM coûteux aux cas limites restants.
Quand activer
- Analyse de texte structuré avec des motifs répétitifs (questions, formulaires, tableaux)
- Décision entre regex et LLM pour l'extraction de texte
- Construction de pipelines hybrides combinant les deux approches
- Optimisation des compromis coût/précision dans le traitement de texte
Cadre de décision
Le format du texte est-il cohérent et répétitif ?
├── Oui (>90 % suit un motif) → Commencer par Regex
│ ├── Regex gère 95%+ → Terminé, pas de LLM nécessaire
│ └── Regex gère <95% → Ajouter LLM uniquement pour les cas limites
└── Non (forme libre, très variable) → Utiliser LLM directement
Architecture du pipeline
Texte source
│
▼
[Parseur Regex] ─── Extrait la structure (précision 95-98%)
│
▼
[Nettoyeur de texte] ─── Supprime le bruit (marqueurs, numéros de page, artefacts)
│
▼
[Scoreur de confiance] ─── Identifie les extractions à faible confiance
│
├── Haute confiance (≥0.95) → Sortie directe
│
└── Faible confiance (<0.95) → [Validateur LLM] → Sortie
Implémentation
1. Parseur Regex (gère la majorité)
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class ParsedItem:
id: str
text: str
choices: tuple[str, ...]
answer: str
confidence: float = 1.0
def parse_structured_text(content: str) -> list[ParsedItem]:
"""Parse le texte structuré à l'aide de motifs regex."""
pattern = re.compile(
r"(?P<id>\d+)\.\s*(?P<text>.+?)\n"
r"(?P<choices>(?:[A-D]\..+?\n)+)"
r"Answer:\s*(?P<answer>[A-D])",
re.MULTILINE | re.DOTALL,
)
items = []
for match in pattern.finditer(content):
choices = tuple(
c.strip() for c in re.findall(r"[A-D]\.\s*(.+)", match.group("choices"))
)
items.append(ParsedItem(
id=match.group("id"),
text=match.group("text").strip(),
choices=choices,
answer=match.group("answer"),
))
return items
2. Score de confiance
Identifie les éléments pouvant nécessiter une révision LLM :
@dataclass(frozen=True)
class ConfidenceFlag:
item_id: str
score: float
reasons: tuple[str, ...]
def score_confidence(item: ParsedItem) -> ConfidenceFlag:
"""Score la confiance d'extraction et signale les problèmes."""
reasons = []
score = 1.0
if len(item.choices) < 3:
reasons.append("few_choices")
score -= 0.3
if not item.answer:
reasons.append("missing_answer")
score -= 0.5
if len(item.text) < 10:
reasons.append("short_text")
score -= 0.2
return ConfidenceFlag(
item_id=item.id,
score=max(0.0, score),
reasons=tuple(reasons),
)
def identify_low_confidence(
items: list[ParsedItem],
threshold: float = 0.95,
) -> list[ConfidenceFlag]:
"""Retourne les éléments en dessous du seuil de confiance."""
flags = [score_confidence(item) for item in items]
return [f for f in flags if f.score < threshold]
3. Validateur LLM (cas limites uniquement)
def validate_with_llm(
item: ParsedItem,
original_text: str,
client,
) -> ParsedItem:
"""Utilise le LLM pour corriger les extractions à faible confiance."""
response = client.messages.create(
model="claude-haiku-4-5-20251001", # Modèle le moins cher pour la validation
max_tokens=500,
messages=[{
"role": "user",
"content": (
f"Extrayez la question, les choix et la réponse de ce texte.\n\n"
f"Texte : {original_text}\n\n"
f"Extraction actuelle : {item}\n\n"
f"Retournez le JSON corrigé si nécessaire, ou 'CORRECT' si exact."
),
}],
)
# Analyser la réponse du LLM et retourner l'élément corrigé...
return corrected_item
4. Pipeline hybride
def process_document(
content: str,
*,
llm_client=None,
confidence_threshold: float = 0.95,
) -> list[ParsedItem]:
"""Pipeline complet : regex -> vérification de confiance -> LLM pour les cas limites."""
# Étape 1 : Extraction par regex (gère 95-98%)
items = parse_structured_text(content)
# Étape 2 : Score de confiance
low_confidence = identify_low_confidence(items, confidence_threshold)
if not low_confidence or llm_client is None:
return items
# Étape 3 : Validation LLM (uniquement pour les éléments signalés)
low_conf_ids = {f.item_id for f in low_confidence}
result = []
for item in items:
if item.id in low_conf_ids:
result.append(validate_with_llm(item, content, llm_client))
else:
result.append(item)
return result
Métriques réelles
Issues d'un pipeline de parsing de quiz en production (410 éléments) :
| Métrique | Valeur |
|---|---|
| Taux de succès regex | 98,0 % |
| Éléments à faible confiance | 8 (2,0 %) |
| Appels LLM nécessaires | ~5 |
| Économies par rapport à tout-LLM | ~95 % |
| Couverture de test | 93 % |
Meilleures pratiques
- Commencez par regex — même une regex imparfaite vous donne une base à améliorer
- Utilisez le score de confiance pour identifier programmatiquement ce qui nécessite l'aide du LLM
- Utilisez le LLM le moins cher pour la validation (les modèles de classe Haiku sont suffisants)
- Ne modifiez jamais les éléments analysés — retournez de nouvelles instances depuis les étapes de nettoyage/validation
- Le TDD fonctionne bien pour les parseurs — écrivez d'abord des tests pour les motifs connus, puis pour les cas limites
- Journalisez les métriques (taux de succès regex, nombre d'appels LLM) pour surveiller la santé du pipeline
Anti-patrons à éviter
- Envoyer tout le texte à un LLM alors que regex gère 95 %+ des cas (coûteux et lent)
- Utiliser regex pour du texte très variable et libre (LLM est meilleur ici)
- Ignorer le score de confiance et espérer que regex « fonctionne tout seul »
- Modifier les objets analysés pendant les étapes de nettoyage/validation
- Ne pas tester les cas limites (entrées mal formées, champs manquants, problèmes d'encodage)
Quand utiliser
- Analyse de questions d'examen/quiz
- Extraction de données de formulaires
- Traitement de factures/reçus
- Analyse de structure de documents (en-têtes, sections, tableaux)
- Tout texte structuré avec des motifs répétitifs où le coût est important