
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ème | Conséquence |
|---|---|
| Majuscules incohérentes | ”Python” et “PYTHON” = 2 tokens différents |
| Caractères invisibles | Chunks corrompus, recherche qui échoue |
| HTML résiduel | Bruit dans les embeddings |
| Ponctuation excessive | Tokens inutiles, coût accru |
Stratégie : quel nettoyage pour quel usage ?
Section intitulée « Stratégie : quel nettoyage pour quel usage ? »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)
Objectif : Trouver des correspondances exactes (BM25, Elasticsearch).
Niveau de nettoyage : Agressif
- ✓ Supprimer le HTML
- ✓ Tout en minuscules
- ✓ Supprimer la ponctuation
- ✓ Supprimer les accents (optionnel)
- ✓ Supprimer les stopwords
Objectif : Extraire des entités, faire du POS tagging, analyser le sentiment.
Niveau de nettoyage : Léger
- ✓ Supprimer le HTML
- ✓ Normaliser les espaces
- ✗ Conserver la ponctuation (nécessaire pour la segmentation)
- ✗ Conserver la casse (utile pour les entités nommées)
Nettoyage avec les outils natifs Python
Section intitulée « Nettoyage avec les outils natifs Python »Avant d’installer des bibliothèques, Python offre des outils puissants pour manipuler les chaînes de caractères.
Étape 1 : Supprimer les espaces inutiles
Section intitulée « Étape 1 : Supprimer les espaces inutiles »texte = " Bonjour, Monde ! "
# strip() supprime les espaces en début et finpropre = 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.
Étape 2 : Normaliser la casse
Section intitulée « Étape 2 : Normaliser la casse »texte = "Bonjour, MONDE !"
# lower() convertit tout en minusculesminuscules = 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”).
Étape 3 : Remplacer des caractères
Section intitulée « Étape 3 : Remplacer des caractères »texte = "Texte avec\ndes sauts\tde ligne"
# replace() remplace une sous-chaînepropre = 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.
Étape 4 : Découper en mots
Section intitulée « Étape 4 : Découper en mots »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èrecsv = "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.
Étape 5 : Supprimer la ponctuation
Section intitulée « Étape 5 : Supprimer la ponctuation »import string
texte = "Bonjour, comment ça va ?"
# string.punctuation contient tous les signes de ponctuationprint(string.punctuation) # !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
# translate() avec maketrans() pour supprimersans_ponct = texte.translate(str.maketrans("", "", string.punctuation))print(sans_ponct) # "Bonjour comment ça va "Ce que fait ce code :
string.punctuationest une chaîne contenant tous les signes de ponctuationstr.maketrans("", "", string.punctuation)crée une table de traduction qui supprime ces caractèrestranslate()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.
Supprimer les balises HTML
Section intitulée « Supprimer les balises HTML »import re
texte = "<p>Bonjour <b>monde</b> !</p>"
# Regex pour supprimer les balises HTMLpropre = 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
Supprimer les caractères invisibles
Section intitulée « Supprimer les caractères invisibles »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 characterspropre = re.sub(r"[\u200b\u200c\u200d\ufeff]", "", texte)print(propre) # "Texte avec caractère invisible ici"Caractères invisibles courants :
| Code | Nom | Source typique |
|---|---|---|
\u200b | Zero-Width Space | Copier-coller web |
\u200c | Zero-Width Non-Joiner | Textes arabes/persans |
\u200d | Zero-Width Joiner | Emojis composés |
\ufeff | BOM (Byte Order Mark) | Fichiers UTF-8 mal encodés |
Normaliser les espaces
Section intitulée « Normaliser les espaces »import re
texte = "Texte avec trop d'espaces"
# Remplacer plusieurs espaces par un seulpropre = 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.
Supprimer les emojis
Section intitulée « Supprimer les emojis »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 ! "Supprimer les accents
Section intitulée « Supprimer les accents »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 :
pip install unidecodePipeline de nettoyage complet
Section intitulée « Pipeline de nettoyage complet »Voici une fonction qui combine toutes les techniques :
import reimport unicodedatafrom 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 texteExemple 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 exacteprint(nettoyer_agressif(texte_html))# "exemple de texte a nettoyer 123 avec des majuscules et des caracteres invisibles"Tokenisation : découper le texte en unités
Section intitulée « Tokenisation : découper le texte en unités »Une fois le texte nettoyé, vous devez le tokeniser — le découper en unités que le modèle peut traiter.
Qu’est-ce qu’un token ?
Section intitulée « Qu’est-ce qu’un token ? »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 :
| Aspect | Impact |
|---|---|
| Coût API | Facturé au nombre de tokens |
| Contexte | Limite de tokens par requête (ex: 128k pour GPT-4) |
| Qualité | Mots rares découpés en sous-parties peuvent perdre du sens |
Tokenisation simple avec split()
Section intitulée « Tokenisation simple avec split() »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.).
Tokenisation avec nltk
Section intitulée « Tokenisation avec nltk »nltk (Natural Language Toolkit) offre des tokenizers linguistiquement corrects :
pip install nltkimport nltknltk.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 phrasesphrases = sent_tokenize(texte, language='french')print(phrases)# ['Bonjour.', 'Le Dr.', "Martin a dit : c'est important !"]
# Découper en motsmots = 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).
Tokenisation avec spaCy
Section intitulée « Tokenisation avec spaCy »spaCy est plus précis que nltk pour l’analyse linguistique :
pip install spacypython -m spacy download fr_core_news_smimport 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 analysefor 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: leDr. | 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
Filtrer les stopwords
Section intitulée « Filtrer les stopwords »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 ponctuationmots_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']Tokenizers LLM : BPE et SentencePiece
Section intitulée « Tokenizers LLM : BPE et SentencePiece »Les LLM modernes utilisent des tokenizers spéciaux qui découpent différemment :
pip install transformersfrom 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é.
Pipeline complet : du texte brut aux chunks RAG
Section intitulée « Pipeline complet : du texte brut aux chunks RAG »Voici un exemple complet qui combine extraction, nettoyage et préparation pour un pipeline RAG :
import reimport jsonimport unicodedatafrom bs4 import BeautifulSoupimport spacy
# Charger le modèle spaCynlp = 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'utilisationtexte_brut = """<html><article><h1>Introduction à l'IA</h1><p>L'intelligence artificielle transforme notre société. Elle permetd'automatiser des tâches complexes. Les applications sont nombreuses.</p><p>Le machine learning est une branche de l'IA. Il utilise des donnéespour apprendre. Les modèles s'améliorent avec l'expérience.</p></article></html>"""
# Pipelinetexte_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] + "...")Dépannage
Section intitulée « Dépannage »UnicodeDecodeError lors de la lecture d’un fichier
Section intitulée « UnicodeDecodeError lors de la lecture d’un fichier »# Spécifiez l'encodage explicitementwith 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èresfor c in texte[:20]: print(f"'{c}' -> U+{ord(c):04X} ({unicodedata.name(c, 'INCONNU')})")spaCy est trop lent sur de gros textes
Section intitulée « spaCy est trop lent sur de gros textes »Désactivez les composants inutiles :
# Charger uniquement le tokenizer et le sentence splitternlp = spacy.load("fr_core_news_sm", disable=["ner", "lemmatizer"])
# Pour encore plus de vitesse, utilisez blanknlp = spacy.blank("fr")nlp.add_pipe("sentencizer")nltk ne trouve pas les données
Section intitulée « nltk ne trouve pas les données »import nltknltk.download('punkt')nltk.download('punkt_tab')nltk.download('stopwords')