Aller au contenu
Développement medium

Nettoyer et préparer du texte pour vos pipelines RAG

24 min de lecture

logo python

Vous avez extrait du texte d’une page web ou d’un PDF, et maintenant ? Avant de l’envoyer à un LLM ou de l’indexer dans un système RAG, vous devez le nettoyer. Un texte “sale” — avec des balises HTML, des espaces en trop, des caractères invisibles — dégrade la qualité de vos embeddings et pollue les réponses de votre assistant.

Ce guide vous montre comment transformer un texte brut inutilisable en texte propre, prêt pour l’analyse NLP ou l’indexation vectorielle.

Le problème : pourquoi le texte brut pose problème

Section intitulée « Le problème : pourquoi le texte brut pose problème »

Quand vous récupérez du texte depuis une source externe (web, PDF, formulaire, base de données), il contient souvent :

" <p>EXEMPLE!!! de texte à nettoyer... 123 € <br>
Avec des MAJUSCULES, des chiffres, des symboles €,
et des caractères invisibles​. 😃</p> "

Ce que vous voyez : du texte avec quelques bizarreries.

Ce que le modèle voit :

  • Des balises HTML (<p>, <br>)
  • Des espaces en début/fin et des tabulations (\t)
  • Des majuscules incohérentes (EXEMPLE, MAJUSCULES)
  • Des caractères invisibles (Zero-Width Space \u200b)
  • Des emojis et symboles (😃, )
  • De la ponctuation répétée (source /tmp/text-processing-test/bin/activate && python3 /tmp/test_tokenizers_modernes.py 2>/dev/null!, ...)

Impact sur votre pipeline RAG :

ProblèmeConséquence
Majuscules incohérentesPython” et “PYTHON” = 2 tokens différents
Caractères invisiblesChunks corrompus, recherche qui échoue
HTML résiduelBruit dans les embeddings
Ponctuation excessiveTokens inutiles, coût accru

Avant de nettoyer, posez-vous la question : que vais-je faire de ce texte ?

Objectif : Indexer le texte pour la recherche sémantique.

Niveau de nettoyage : Modéré

  • ✓ Supprimer le HTML
  • ✓ Normaliser les espaces
  • ✓ Supprimer les caractères invisibles
  • ✗ Ne pas supprimer la ponctuation (contexte utile)
  • ✗ Ne pas mettre en minuscules (les LLM gèrent)

Avant d’installer des bibliothèques, Python offre des outils puissants pour manipuler les chaînes de caractères.

texte = " Bonjour, Monde ! "
# strip() supprime les espaces en début et fin
propre = texte.strip()
print(propre) # "Bonjour, Monde !"

Ce que fait strip() : Supprime les espaces, tabulations et sauts de ligne en début et fin de chaîne. Utile car les textes extraits ont souvent des espaces parasites.

texte = "Bonjour, MONDE !"
# lower() convertit tout en minuscules
minuscules = texte.lower()
print(minuscules) # "bonjour, monde !"
# upper() convertit tout en majuscules (moins courant)
majuscules = texte.upper()
print(majuscules) # "BONJOUR, MONDE !"

Quand utiliser lower() : Pour la recherche exacte ou quand vous voulez unifier “Python”, “PYTHON” et “python” en un seul token.

Quand ne pas l’utiliser : Pour l’analyse NLP où la casse aide à identifier les noms propres (“Paris” vs “paris”).

texte = "Texte avec\ndes sauts\tde ligne"
# replace() remplace une sous-chaîne
propre = texte.replace("\n", " ").replace("\t", " ")
print(propre) # "Texte avec des sauts de ligne"

Utilisation courante : Remplacer les sauts de ligne (\n) et tabulations (\t) par des espaces pour avoir un texte sur une seule ligne.

texte = "Voici un exemple de texte"
# split() découpe selon un délimiteur (espace par défaut)
mots = texte.split()
print(mots) # ['Voici', 'un', 'exemple', 'de', 'texte']
# Découper sur un autre caractère
csv = "pomme,poire,banane"
fruits = csv.split(",")
print(fruits) # ['pomme', 'poire', 'banane']

Limitation : split() ne gère pas la ponctuation attachée aux mots. Pour “Bonjour, monde!”, vous obtenez ['Bonjour,', 'monde!'] au lieu de séparer la ponctuation.

import string
texte = "Bonjour, comment ça va ?"
# string.punctuation contient tous les signes de ponctuation
print(string.punctuation) # !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
# translate() avec maketrans() pour supprimer
sans_ponct = texte.translate(str.maketrans("", "", string.punctuation))
print(sans_ponct) # "Bonjour comment ça va "

Ce que fait ce code :

  1. string.punctuation est une chaîne contenant tous les signes de ponctuation
  2. str.maketrans("", "", string.punctuation) crée une table de traduction qui supprime ces caractères
  3. translate() applique cette table

Nettoyage avancé avec les expressions régulières

Section intitulée « Nettoyage avancé avec les expressions régulières »

Pour des nettoyages plus complexes, les expressions régulières (regex) sont indispensables.

import re
texte = "<p>Bonjour <b>monde</b> !</p>"
# Regex pour supprimer les balises HTML
propre = re.sub(r"<[^>]+>", "", texte)
print(propre) # "Bonjour monde !"

Explication de la regex <[^>]+> :

  • < : commence par un chevron ouvrant
  • [^>]+ : un ou plusieurs caractères qui ne sont pas >
  • > : se termine par un chevron fermant

Certains caractères sont invisibles mais perturbent le traitement :

import re
texte = "Texte avec\u200b caractère\u200c invisible\u200d ici"
# Supprimer les Zero-Width characters
propre = re.sub(r"[\u200b\u200c\u200d\ufeff]", "", texte)
print(propre) # "Texte avec caractère invisible ici"

Caractères invisibles courants :

CodeNomSource typique
\u200bZero-Width SpaceCopier-coller web
\u200cZero-Width Non-JoinerTextes arabes/persans
\u200dZero-Width JoinerEmojis composés
\ufeffBOM (Byte Order Mark)Fichiers UTF-8 mal encodés
import re
texte = "Texte avec trop d'espaces"
# Remplacer plusieurs espaces par un seul
propre = re.sub(r"\s+", " ", texte).strip()
print(propre) # "Texte avec trop d'espaces"

Explication : \s+ correspond à un ou plusieurs espaces (espace, tab, saut de ligne). On les remplace tous par un seul espace.

import re
texte = "Super article ! 😃👍🎉"
# Supprimer les emojis (plage Unicode simplifiée)
propre = re.sub(r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF]", "", texte)
print(propre) # "Super article ! "

Parfois utile pour la recherche, mais détruit de l’information :

from unidecode import unidecode
texte = "Éducation numérique : ça évolue vite !"
sans_accents = unidecode(texte)
print(sans_accents) # "Education numerique : ca evolue vite !"

Installation :

Fenêtre de terminal
pip install unidecode

Voici une fonction qui combine toutes les techniques :

import re
import unicodedata
from bs4 import BeautifulSoup
def nettoyer_pour_rag(texte: str) -> str:
"""
Nettoie un texte pour un pipeline RAG.
Niveau de nettoyage modéré : supprime le bruit
mais conserve la structure et la ponctuation.
"""
# 1. Supprimer le HTML
if "<" in texte and ">" in texte:
texte = BeautifulSoup(texte, "html.parser").get_text(separator=" ")
# 2. Normaliser Unicode (NFC = forme canonique)
texte = unicodedata.normalize("NFC", texte)
# 3. Supprimer les caractères de contrôle (sauf sauts de ligne)
texte = ''.join(
c if c == '\n' or not unicodedata.category(c).startswith('C')
else ' '
for c in texte
)
# 4. Normaliser les espaces
texte = re.sub(r'[ \t]+', ' ', texte) # Espaces multiples -> un
texte = re.sub(r'\n{3,}', '\n\n', texte) # Max 2 sauts de ligne
texte = texte.strip()
return texte
def nettoyer_agressif(texte: str) -> str:
"""
Nettoyage agressif pour la recherche exacte.
Minuscules, sans ponctuation, sans accents.
"""
from unidecode import unidecode
texte = nettoyer_pour_rag(texte)
texte = texte.lower()
texte = re.sub(r'[^\w\s]', '', texte) # Supprime ponctuation
texte = unidecode(texte) # Supprime accents
texte = re.sub(r'\s+', ' ', texte).strip()
return texte

Exemple d’utilisation :

texte_html = """
<p>EXEMPLEsource /tmp/text-processing-test/bin/activate && python3 /tmp/test_tokenizers_modernes.py 2>/dev/null! de texte à nettoyer... 123 € <br>
Avec des MAJUSCULES et des caractères​ invisibles. 😃</p>
"""
# Pour RAG (conserve structure)
print(nettoyer_pour_rag(texte_html))
# "EXEMPLE!!! de texte à nettoyer... 123 € Avec des MAJUSCULES et des caractères invisibles. 😃"
# Pour recherche exacte
print(nettoyer_agressif(texte_html))
# "exemple de texte a nettoyer 123 avec des majuscules et des caracteres invisibles"

Une fois le texte nettoyé, vous devez le tokeniser — le découper en unités que le modèle peut traiter.

Un token est la plus petite unité de texte traitée par un modèle. Selon le tokenizer utilisé, un token peut être :

  • Un mot : “intelligence” → 1 token
  • Un sous-mot : “artificielle” → “artific” + “ielle” (2 tokens)
  • Un caractère : “IA” → “I” + “A” (2 tokens)

Pourquoi c’est important :

AspectImpact
Coût APIFacturé au nombre de tokens
ContexteLimite de tokens par requête (ex: 128k pour GPT-4)
QualitéMots rares découpés en sous-parties peuvent perdre du sens

La méthode split() découpe sur les espaces :

texte = "L'intelligence artificielle transforme notre façon de travailler."
mots = texte.split()
print(mots)
# ["L'intelligence", "artificielle", "transforme", "notre", "façon", "de", "travailler."]

Limitation : La ponctuation reste attachée aux mots (travailler.).

nltk (Natural Language Toolkit) offre des tokenizers linguistiquement corrects :

Fenêtre de terminal
pip install nltk
import nltk
nltk.download('punkt', quiet=True)
nltk.download('punkt_tab', quiet=True)
from nltk.tokenize import word_tokenize, sent_tokenize
texte = "Bonjour. Le Dr. Martin a dit : c'est important !"
# Découper en phrases
phrases = sent_tokenize(texte, language='french')
print(phrases)
# ['Bonjour.', 'Le Dr.', "Martin a dit : c'est important !"]
# Découper en mots
mots = word_tokenize(texte, language='french')
print(mots)
# ['Bonjour', '.', 'Le', 'Dr', '.', 'Martin', 'a', 'dit', ':', "c'est", 'important', '!']

Avantage : Sépare la ponctuation des mots.

Limitation : Gère mal certaines abréviations françaises (“Dr.” découpé comme fin de phrase).

spaCy est plus précis que nltk pour l’analyse linguistique :

Fenêtre de terminal
pip install spacy
python -m spacy download fr_core_news_sm
import spacy
nlp = spacy.load("fr_core_news_sm")
doc = nlp("Bonjour. Le Dr. Martin a dit : c'est important !")
# Découper en phrases (gère mieux les abréviations)
phrases = [sent.text for sent in doc.sents]
print(phrases)
# ['Bonjour.', 'Le Dr. Martin a dit :', "c'est important !"]
# Découper en tokens avec analyse
for token in doc:
print(f"{token.text:15} | POS: {token.pos_:8} | Lemme: {token.lemma_}")

Sortie :

Bonjour | POS: PROPN | Lemme: Bonjour
. | POS: PUNCT | Lemme: .
Le | POS: DET | Lemme: le
Dr. | POS: NOUN | Lemme: dr.
Martin | POS: PROPN | Lemme: Martin
...

Avantages de spaCy :

  • Gère correctement “Dr.” comme une abréviation
  • Fournit le POS (Part-of-Speech) : nom, verbe, adjectif…
  • Fournit le lemme : forme de base du mot
  • Détecte les entités nommées

Les stopwords sont des mots très fréquents qui apportent peu de sens : “le”, “de”, “et”, “à”…

import spacy
nlp = spacy.load("fr_core_news_sm")
doc = nlp("L'intelligence artificielle transforme notre façon de travailler.")
# Filtrer stopwords et ponctuation
mots_importants = [
token.text for token in doc
if not token.is_stop and not token.is_punct and not token.is_space
]
print(mots_importants)
# ['intelligence', 'artificielle', 'transforme', 'façon', 'travailler']

Les LLM modernes utilisent des tokenizers spéciaux qui découpent différemment :

Fenêtre de terminal
pip install transformers
from transformers import AutoTokenizer
texte = "L'intelligence artificielle transforme notre façon de travailler."
# CamemBERT (modèle français)
tokenizer = AutoTokenizer.from_pretrained("camembert-base")
tokens = tokenizer.tokenize(texte)
print(f"CamemBERT ({len(tokens)} tokens): {tokens}")
# CamemBERT (10 tokens): ['▁L', "'", 'intelligence', '▁artificielle', ...]
# GPT-2 (modèle anglophone)
tokenizer_gpt = AutoTokenizer.from_pretrained("gpt2")
tokens_gpt = tokenizer_gpt.tokenize(texte)
print(f"GPT-2 ({len(tokens_gpt)} tokens): {tokens_gpt}")
# GPT-2 (18 tokens): ['L', "'", 'intelligence', 'Ġartific', 'iel', 'le', ...]

Observation importante : Pour du texte français, CamemBERT (entraîné sur du français) utilise 10 tokens là où GPT-2 en utilise 18. Choisissez un modèle adapté à votre langue pour réduire les coûts et améliorer la qualité.

Voici un exemple complet qui combine extraction, nettoyage et préparation pour un pipeline RAG :

import re
import json
import unicodedata
from bs4 import BeautifulSoup
import spacy
# Charger le modèle spaCy
nlp = spacy.load("fr_core_news_sm")
def nettoyer_texte(texte: str) -> str:
"""Nettoie le texte pour RAG."""
if "<" in texte and ">" in texte:
texte = BeautifulSoup(texte, "html.parser").get_text(separator=" ")
texte = unicodedata.normalize("NFC", texte)
texte = ''.join(
c if c == '\n' or not unicodedata.category(c).startswith('C')
else ' '
for c in texte
)
texte = re.sub(r'[ \t]+', ' ', texte)
texte = re.sub(r'\n{3,}', '\n\n', texte)
return texte.strip()
def segmenter_en_phrases(texte: str) -> list[str]:
"""Découpe le texte en phrases avec spaCy."""
doc = nlp(texte)
return [sent.text.strip() for sent in doc.sents if sent.text.strip()]
def creer_chunks(phrases: list[str], taille_max: int = 500) -> list[dict]:
"""
Regroupe les phrases en chunks de taille maximale.
Chaque chunk contient des phrases complètes et ne dépasse pas
taille_max caractères.
"""
chunks = []
chunk_actuel = []
taille_actuelle = 0
for phrase in phrases:
taille_phrase = len(phrase)
if taille_actuelle + taille_phrase > taille_max and chunk_actuel:
# Sauvegarder le chunk actuel
chunks.append({
"id": len(chunks),
"texte": " ".join(chunk_actuel),
"nb_phrases": len(chunk_actuel)
})
chunk_actuel = []
taille_actuelle = 0
chunk_actuel.append(phrase)
taille_actuelle += taille_phrase + 1 # +1 pour l'espace
# Dernier chunk
if chunk_actuel:
chunks.append({
"id": len(chunks),
"texte": " ".join(chunk_actuel),
"nb_phrases": len(chunk_actuel)
})
return chunks
# Exemple d'utilisation
texte_brut = """
<html>
<article>
<h1>Introduction à l'IA</h1>
<p>L'intelligence artificielle transforme notre société. Elle permet
d'automatiser des tâches complexes. Les applications sont nombreuses.</p>
<p>Le machine learning est une branche de l'IA. Il utilise des données
pour apprendre. Les modèles s'améliorent avec l'expérience.</p>
</article>
</html>
"""
# Pipeline
texte_propre = nettoyer_texte(texte_brut)
phrases = segmenter_en_phrases(texte_propre)
chunks = creer_chunks(phrases, taille_max=200)
print(f"Texte nettoyé: {len(texte_propre)} caractères")
print(f"Phrases: {len(phrases)}")
print(f"Chunks: {len(chunks)}")
for chunk in chunks:
print(f"\n--- Chunk {chunk['id']} ({chunk['nb_phrases']} phrases) ---")
print(chunk['texte'][:100] + "...")

UnicodeDecodeError lors de la lecture d’un fichier

Section intitulée « UnicodeDecodeError lors de la lecture d’un fichier »
# Spécifiez l'encodage explicitement
with open("fichier.txt", "r", encoding="utf-8") as f:
texte = f.read()
# Si ça échoue, essayez avec errors="ignore" ou "replace"
with open("fichier.txt", "r", encoding="utf-8", errors="replace") as f:
texte = f.read()

Le texte contient des caractères bizarres après nettoyage

Section intitulée « Le texte contient des caractères bizarres après nettoyage »

Vérifiez la normalisation Unicode :

import unicodedata
# Afficher les codes des caractères
for c in texte[:20]:
print(f"'{c}' -> U+{ord(c):04X} ({unicodedata.name(c, 'INCONNU')})")

Désactivez les composants inutiles :

# Charger uniquement le tokenizer et le sentence splitter
nlp = spacy.load("fr_core_news_sm", disable=["ner", "lemmatizer"])
# Pour encore plus de vitesse, utilisez blank
nlp = spacy.blank("fr")
nlp.add_pipe("sentencizer")
import nltk
nltk.download('punkt')
nltk.download('punkt_tab')
nltk.download('stopwords')

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.