Aller au contenu principal

Module 2 — Extraction de texte : PDF, HTML, documents bureautiques

Un système RAG ne cherche pas dans des fichiers, il cherche dans du texte. La première étape est donc d'extraire, de chacun des 300 documents du fil rouge, un texte propre auquel on peut appliquer les modules suivants. Le mot « propre » cache la moitié des ennuis qui viendront ensuite : c'est le module le plus ingrat, et celui qui décide du plafond de qualité de tout le reste.

Pourquoi on ne récupère jamais simplement le texte

Chaque format porte ses pathologies. Un PDF n'est pas un document au sens habituel : c'est un jeu de commandes graphiques positionnant des glyphes sur une page. Rien, dans le format, ne dit dans quel ordre lire un tableau à deux colonnes ou où finit un paragraphe. Le HTML d'un intranet moderne mélange une navigation, un pied de page et un bandeau publicitaire au texte utile. Un fichier bureautique cache des révisions, des commentaires, et des retours à la ligne insérés par un rédacteur pour tenir dans une case.

L'extraction consiste à reconstituer, dans chaque cas, la séquence de mots qu'un humain aurait lue à voix haute, plus les métadonnées qui permettront plus tard de retrouver la page exacte.

PDF « texte » : pdfplumber

Un PDF issu d'un traitement de texte contient les glyphes sous forme de caractères, avec leur position. pdfplumber reconstitue les lignes et les tableaux en analysant ces positions.

import pdfplumber

def extraire_pdf(chemin):
passages = []
with pdfplumber.open(chemin) as pdf:
for numero, page in enumerate(pdf.pages, start=1):
texte = page.extract_text() or ""
tableaux = page.extract_tables()
passages.append({
"source": chemin,
"page": numero,
"texte": texte,
"tableaux": tableaux,
})
return passages

Deux points sont critiques et rarement mentionnés. D'abord, extract_text sans réglages peut mêler les colonnes d'une page à deux colonnes : lire de gauche à droite ligne par ligne saute d'une colonne à l'autre. Il faut alors passer par page.extract_words puis reconstituer les lignes en tenant compte de la position horizontale. Ensuite, un tableau doit être sérialisé séparément du texte, sinon les cellules apparaissent en séquence continue et perdent leur sens : « Nom Prénom Âge Alice Dupont 32 Bob Martin 45 ».

PDF scanné : OCR par Tesseract

Une facture archivée, un règlement signé, un scan d'ancien document se présentent sous forme d'images encapsulées dans un PDF. Aucune extraction textuelle n'y trouve de mots ; il faut passer par la reconnaissance optique de caractères.

from pdf2image import convert_from_path
import pytesseract

def ocr_pdf(chemin, langue="fra"):
passages = []
for numero, image in enumerate(convert_from_path(chemin, dpi=300), start=1):
texte = pytesseract.image_to_string(image, lang=langue)
passages.append({"source": chemin, "page": numero, "texte": texte})
return passages

La résolution compte : à 150 points par pouce, Tesseract confond des « rn » avec des « m » et des « 5 » avec des « S ». Trois cents points par pouce est un bon compromis entre qualité et vitesse. Pour l'arabe et le français mêlés, préciser lang="ara+fra".

Ne pas mélanger extraction textuelle et OCR aveuglément

Un PDF est parfois « à moitié scanné » : quelques pages sont du texte, d'autres des images. Appeler l'OCR sur les premières produit un texte médiocre alors qu'un texte parfait était disponible. La bonne heuristique est de tenter d'abord l'extraction textuelle ; si le résultat pour une page contient moins d'une trentaine de caractères ou un ratio de lettres suspectes, on rebascule sur l'OCR pour cette page seulement.

HTML : ne prendre que le texte utile

Un page.get_text() naïf d'une page d'intranet mélange le menu latéral, le fil d'Ariane et le pied de page au contenu. La recherche remontera ensuite le menu à chaque question, parce qu'il apparaît sur toutes les pages.

La bibliothèque trafilatura isole le corps d'article. Elle a été calibrée sur des dizaines de milliers de pages et fait mieux que n'importe quelle règle ad hoc.

import trafilatura

def extraire_html(url_ou_html):
html = trafilatura.fetch_url(url_ou_html) if url_ou_html.startswith("http") else url_ou_html
texte = trafilatura.extract(
html,
include_tables=True,
include_links=False,
favor_precision=True,
)
return texte

Le drapeau favor_precision=True sacrifie un peu de rappel pour éviter d'attraper la navigation ; sur un corpus d'entreprise où l'on peut se permettre de réextraire, c'est presque toujours le bon réglage.

Documents bureautiques

Les traitements de texte modernes exposent le contenu sous forme de paragraphes typés. Il ne faut pas s'en priver, car « titre de section » est une information précieuse pour le découpage du module 3.

from docx import Document

def extraire_docx(chemin):
doc = Document(chemin)
passages = []
section_courante = None
for paragraphe in doc.paragraphs:
style = paragraphe.style.name
if style.startswith("Heading"):
section_courante = paragraphe.text
continue
if paragraphe.text.strip():
passages.append({
"source": chemin,
"section": section_courante,
"texte": paragraphe.text,
})
return passages

Pour les tableurs, on lit chaque feuille avec openpyxl et l'on sérialise ligne par ligne en préfixant les valeurs par le nom de colonne : « Congés du 12 mai au 20 mai — statut : approuvé ». Une cellule isolée n'a aucun sens pour une recherche textuelle.

Métadonnées à conserver absolument

L'extraction ne produit pas seulement du texte : elle produit du texte avec un contexte de retour. À chaque passage, on garde au minimum le chemin du fichier source, la page (pour un PDF) ou la section (pour un document bureautique), la date de dernière modification, et le niveau de confidentialité tel qu'il apparaît dans les propriétés du fichier ou dans son emplacement.

metadonnees = {
"source": chemin,
"page": 4,
"section": "Conges et absences",
"date_maj": "2026-03-14",
"confidentialite": "interne",
}

Ce sont ces champs qui permettront au module 4 de filtrer par confidentialité, au module 7 de citer « procédure RH v3.1, page 4, section Congés et absences », et au module 9 de repérer qu'un document a été mis à jour et doit être réindexé.

Nettoyer sans dénaturer

Les en-têtes et pieds de page d'un PDF reviennent identiques à toutes les pages ; en garder cinq cents copies dans l'index gonfle les résultats sans rien apporter. Un simple comptage — tout ligne qui apparaît sur plus de la moitié des pages du document est probablement un en-tête — les retire sans risque. Ne pas supprimer en revanche les accents, la casse, ou la ponctuation : le module 5 en aura besoin pour la recherche lexicale et pour la lisibilité des citations.

En résumé

  • Un PDF n'est pas un document textuel : pdfplumber reconstitue les lignes et les tableaux à extraire séparément, et un scan doit passer par OCR à 300 points par pouce.
  • Une page HTML doit être débarrassée de la navigation avec trafilatura, sinon le menu remontera à chaque requête.
  • Les documents bureautiques exposent les styles : on garde les titres de section comme contexte et l'on sérialise les tableurs ligne par ligne.
  • Chaque passage est enregistré avec ses métadonnées — source, page, section, date, confidentialité — car elles seront indispensables au filtrage, à la citation et à la réindexation dans les modules qui suivent.

Module suivant : découper ces textes en morceaux de taille utile, sans casser une phrase clef en deux.