Les données brutes issues du web scraping atterrissent presque jamais dans un état qu'un modèle peut utiliser. Récupérez quelques milliers de pages de produits, d'annonces ou d'articles et vous obtenez des lignes dupliquées, des prix stockés en chaînes, des dates dans cinq formats, des cellules vides, et du texte parsemé d'entités HTML et d'espaces parasites. Alimentez cela directement dans une tâche d'entraînement et le modèle apprend le bruit aussi facilement que le signal. Le travail qui transforme un scrape en jeu de données, c'est le nettoyage et la structuration des données, et c'est là que se gagne ou se perd la majeure partie de la précision d'un pipeline AI ou ML.
Ce guide est un tutoriel pratique pour structurer et nettoyer les données web pour l'AI : charger un scrape brut, le dédupliquer, normaliser les types et formats, gérer les valeurs manquantes, effectuer un nettoyage de texte basique et une tokenisation, concevoir un schéma sur lequel votre code en aval peut compter, et valider le résultat avant qu'il n'atteigne jamais un modèle. Chaque étape dispose de code Python exécutable que vous pouvez coller dans un notebook. À la fin, nous indiquons où Crawlbase peut renvoyer une sortie propre et structurée d'emblée pour que vous fassiez moins de cela manuellement.
Pourquoi le nettoyage et la structuration décident de la qualité du modèle
Les modèles ne raisonnent pas sur vos données comme vous le faites. Une ligne dupliquée est un poids supplémentaire sur un exemple. Un prix stocké comme "$1,299.00" est une chaîne que le modèle ne peut pas comparer à 1299.0. Une date écrite comme "03/04/2025" dans certaines lignes et "2025-04-03" dans d'autres devient deux tokens non liés. Aucun de ces problèmes ne génère d'erreur, ce qui est exactement pourquoi ils sont dangereux : le pipeline s'exécute, les métriques semblent plausibles, et le modèle apprend silencieusement depuis une vue corrompue du monde.
Le nettoyage corrige les dommages évidents (doublons, valeurs manquantes, formats incohérents) et la structuration impose un contrat : chaque colonne a un type, une unité et une signification. Ce contrat est ce qui permet au même jeu de données d'alimenter un classifieur aujourd'hui et un modèle différent le trimestre prochain sans surprise. La même discipline s'applique que vous fassiez du web scraping pour le machine learning sur quelques milliers de lignes ou que vous lanciez un scraping web à grande échelle dans les millions.
Commencer avec un scrape brut réaliste
Pour rendre les étapes concrètes, supposez que vous avez scrapé un ensemble d'annonces de produits e-commerce et les avez écrites dans raw_products.csv. Un vrai scrape est désordonné, donc le fichier ci-dessous l'est aussi : lignes dupliquées, symboles monétaires et séparateurs de milliers dans price, formats de date mixtes, cellules vides, et texte d'avis avec des entités HTML et des espaces irréguliers.
import pandas as pd df = pd.read_csv("raw_products.csv") # A first look at the damage before touching anything print(df.shape) print(df.dtypes) print(df.isna().sum()) print(df.head())
La sortie dtypes est le signe révélateur. Si price revient comme object au lieu d'un type numérique, pandas n'a pas pu l'analyser, ce qui signifie qu'il y a des données non numériques dans la colonne. Exécuter isna().sum() tôt vous indique quelles colonnes ont des valeurs manquantes et à quel point c'est grave, afin que vous puissiez décider quoi corriger avant d'écrire une seule transformation.
Dédupliquer d'abord
La déduplication vient avant tout le reste car les doublons gonflent chaque statistique ultérieure. Les doublons exacts sont le cas facile : des lignes identiques d'un scraper qui a revisité la même URL ou paginé sur une fenêtre qui se chevauche.
# Drop fully identical rows df = df.drop_duplicates() # Deduplicate on a business key, keeping the most recent capture df = (df.sort_values("scraped_at") .drop_duplicates(subset=["product_id"], keep="last"))
Le deuxième schéma compte plus en pratique. Deux captures du même product_id ne sont pas des lignes identiques une fois que le prix a changé, mais vous voulez généralement un enregistrement par produit, pas deux. Trier par heure de capture et garder "last" vous donne la version la plus récente. Choisissez la clé qui identifie réellement une entité dans votre domaine (un ID produit, une URL, un SKU) plutôt que de faire confiance à l'égalité de lignes complètes.
L'ordre n'est pas cosmétique ici. Si vous remplissez les valeurs manquantes d'abord et déduplication ensuite, vos moyennes imputées sont calculées sur des comptages gonflés et biaisées vers les entités les plus dupliquées. Supprimez toujours les doublons avant toute statistique (moyenne, médiane, mode) dont dépendent les étapes ultérieures.
Normaliser les types et formats
Avec une ligne par entité, faites de chaque colonne un type unique et prévisible. C'est l'étape qui transforme "$1,299.00" en 1299.0 et cinq formats de date en un seul. Les chaînes monétaires ont besoin de leurs symboles et séparateurs retirés avant de s'analyser en nombres ; les dates ont besoin d'un seul parser avec errors="coerce" pour que les données non analysables deviennent NaT au lieu de faire planter l'exécution.
# Strip currency symbols and separators, then parse to float df["price"] = (df["price"] .astype("string") .str.replace(r"[^\d.]", "", regex=True)) df["price"] = pd.to_numeric(df["price"], errors="coerce") # Parse mixed date formats into one datetime type df["listed_on"] = pd.to_datetime( df["listed_on"], errors="coerce" ) # Normalize a categorical column: trim and lowercase df["category"] = df["category"].str.strip().str.lower()
Normaliser les catégorielles est le gain discret ici. Les scrapes produisent régulièrement "Electronics", "electronics " et "ELECTRONICS" comme trois valeurs distinctes pour une seule catégorie. Supprimer les espaces et mettre en minuscules les regroupe en une seule, ce qui signifie des résultats group-by plus propres et une seule feature au lieu de trois après encodage. Faites la même standardisation sur les unités : si certains prix sont en dollars et d'autres en centimes, convertissez en une seule unité maintenant, pendant que vous vous souvenez encore laquelle est laquelle.
Gérer les valeurs manquantes délibérément
Il n'y a pas de règle universelle pour les données manquantes, seulement des compromis. Supprimer les lignes est sûr quand les lacunes sont rares et que la ligne est inutile sans le champ. L'imputation conserve la ligne mais invente une valeur, donc elle n'a de sens que quand la colonne est nécessaire et qu'une estimation raisonnable existe. Décidez colonne par colonne plutôt qu'en appliquant un seul appel global à tout le frame.
# Drop rows missing a field the record cannot exist without df = df.dropna(subset=["product_id", "price"]) # Impute a numeric column with its median (robust to outliers) df["rating"] = df["rating"].fillna(df["rating"].median()) # Fill a categorical with an explicit sentinel, not a guess df["brand"] = df["brand"].fillna("unknown")
La médiane bat la moyenne pour imputer les colonnes numériques car quelques valeurs aberrantes extrêmes (un prix mal scrapé de 99999) tirent la moyenne mais déplacent à peine la médiane. Pour les catégorielles, un sentinel explicite "unknown" est honnête : il dit au modèle que la valeur était absente plutôt que de prétendre qu'elle appartenait à la catégorie la plus courante. Ne laissez jamais un encodeur en aval traiter silencieusement NaN comme une vraie catégorie.
Nettoyer le texte et tokeniser
Si votre jeu de données contient du texte libre (descriptions de produits, avis, corps d'articles), il nécessite sa propre passe. Le texte scrapé arrive avec des entités HTML (&, '), des balises résiduelles, des URLs et des espaces incohérents. Nettoyez-le avant de tokeniser ou les tokens seront remplis de déchets. La tokenisation ici signifie découper le texte en unités qu'un modèle consomme ; l'exemple ci-dessous effectue une tokenisation par espaces, ce qui est suffisant pour illustrer le nettoyage qui doit venir en premier.
import re import html def clean_text(value): if pd.isna(value): return "" text = html.unescape(str(value)) # & -> & text = re.sub(r"<[^>]+>", " ", text) # strip tags text = re.sub(r"http\S+", " ", text) # strip URLs text = re.sub(r"\s+", " ", text) # collapse whitespace return text.strip().lower() df["review_clean"] = df["review"].apply(clean_text) df["tokens"] = df["review_clean"].str.split()
L'ordre dans clean_text est délibéré : désescaper les entités d'abord pour que &amp; devienne & avant qu'une regex s'exécute, supprimer les balises et les URLs ensuite, puis réduire les espaces en dernier pour que les lacunes laissées par les suppressions précédentes se ferment. Mettre en minuscules à la fin maintient les comptages de tokens honnêtes ("Fast" et "fast" deviennent un seul token). Pour un vrai travail NLP, vous remplaceriez le split final par un vrai tokeniseur, mais le nettoyage ci-dessus est la partie que les données scrapées nécessitent toujours.
La majorité de la suppression d'entités ci-dessus existe parce que les scrapes HTML bruts sont bruyants. La Crawling API peut renvoyer une sortie propre et structurée (y compris une vue markdown de la page) pour que le texte arrive sans balises ni texte générique, réduisant l'étape de nettoyage à la normalisation de types. Pointez-la vers une page publique sur le niveau gratuit et comparez la sortie à une récupération brute.
Concevoir un schéma et l'appliquer
Jusqu'ici, le nettoyage a été réactif. Un schéma en fait un contrat : un ensemble déclaré de colonnes, chacune avec un seul type, que chaque lot doit satisfaire. Encoder le schéma comme des types vers lesquels vous castez (et comme des assertions que vous vérifiez) signifie que le prochain scrape soit se conforme, soit échoue bruyamment, au lieu de dériver silencieusement dans le même désordre que vous venez de nettoyer.
# A schema is just a column -> dtype contract schema = { "product_id": "string", "category": "category", "price": "float64", "rating": "float64", "brand": "string", "listed_on": "datetime64[ns]", } # Keep only schema columns, in order, and cast each one df = df[list(schema.keys())].astype(schema)
Sélectionner list(schema.keys()) supprime toutes les colonnes parasites que le scraper a ajoutées et fixe l'ordre des colonnes, pour que chaque export ait la même forme. L'appel astype(schema) caste chaque colonne et lèvera une erreur si une valeur ne peut pas être coercée, ce qui est le comportement que vous voulez : mieux un échec bruyant maintenant qu'une colonne corrompue découverte après un cycle d'entraînement. Utiliser le dtype category pour les champs à faible cardinalité comme category réduit également la mémoire et accélère les group-bys sur les grands frames.
Valider avant d'exporter
La validation est la porte entre "semble propre" et "est propre". Quelques assertions détectent les échecs qui empoisonnent silencieusement un modèle : doublons survivants, nombres hors plage, nulls dans des colonnes qui devraient être complètes. Exécutez-les sur chaque lot et arrêtez le pipeline quand l'une échoue.
def validate(frame): assert frame["product_id"].is_unique, "duplicate product_id" assert frame["price"].between(0, 100000).all(), "price out of range" assert frame["rating"].between(0, 5).all(), "rating out of range" assert frame[["product_id", "price"]].notna().all().all(), "unexpected nulls" return frame df = validate(df)
Les vérifications de plage valent leur place : une note de 50 sur une échelle de 0 à 5 ou un prix négatif est presque toujours un bug d'analyse d'une étape précédente, et l'assertion le fait apparaître avant que les données n'atteignent un modèle. Si vous dépassez les assertions manuelles, une bibliothèque de validation de schéma comme Pandera ou Great Expectations exprime les mêmes règles de façon déclarative, mais les assertions ci-dessus suffisent à rendre un pipeline fiable.
Exporter le jeu de données propre
Avec le frame dédupliqué, normalisé, imputé, casté selon le schéma, et validé, écrivez-le dans un format qui préserve vos types. CSV est portable mais typé en chaînes ; Parquet conserve les dtypes, compresse bien et se charge plus vite, ce qui importe une fois que vous faites de l'entraînement de modèle AI sur le résultat.
# Parquet preserves dtypes and is fast to reload df.to_parquet("clean_products.parquet", index=False) # CSV if you need maximum portability df.to_csv("clean_products.csv", index=False)
Ce fichier est maintenant un jeu de données, pas un scrape : une ligne par entité, un type par colonne, aucun doublon survivant, valeurs manquantes traitées délibérément, et chaque valeur dans sa plage déclarée. De là, le chemin vers un modèle est le familier : feature engineering et split train/test, et les données en dessous ne vous surprendront pas.
Laisser la source renvoyer des données plus propres
L'étape de nettoyage la plus rapide est celle que vous sautez parce que les données sont arrivées propres. Beaucoup du travail ci-dessus (désescapage d'entités, suppression de balises, réduction d'espaces) n'existe que parce que vous avez scrapé du HTML brut. La Crawling API peut renvoyer une vue propre en style markdown d'une page pour que le texte arrive sans balises ni texte générique, et la Crawling API analyse automatiquement de nombreux sites populaires en champs JSON structurés, ce qui supprime l'écriture de sélecteurs et la majeure partie des suppositions de types avant que pandas ne voie les données. Quand vous avez besoin d'adresses IP résidentielles rotatives sans gérer un pool, le Smart AI Proxy couvre cet aspect.
Rien de cela ne supprime le besoin de dédupliquer, valider et appliquer un schéma (ce sont des propriétés de votre jeu de données, pas de la page), mais cela réduit la première moitié bruyante du travail. Pour où va ce jeu de données ensuite, voir comment fonctionne l'extraction de données AI et, pour la capture à volume élevé, les schémas dans le scraping web e-commerce.
Points clés
- Dédupliquer d'abord. Supprimez les doublons avant toute statistique, sinon les moyennes imputées sont calculées sur des comptages gonflés et biaisés.
-
Normaliser les types et formats. Retirez les symboles monétaires vers des floats, analysez les dates mixtes avec
errors="coerce", et nettoyez et mettez en minuscules les catégorielles. -
Gérer les valeurs manquantes par colonne. Supprimez les lignes manquant d'un champ essentiel, imputez les numériques avec la médiane, et remplissez les catégorielles avec un sentinel
"unknown"explicite. - Nettoyer le texte avant de tokeniser. Désescapez les entités, supprimez les balises et URLs, puis réduisez les espaces, dans cet ordre.
- Définir un schéma et valider. Castez chaque colonne vers un type déclaré et vérifiez l'unicité, les plages et le non-null sur chaque lot pour que les mauvaises données échouent bruyamment.
- Une entrée plus propre signifie moins de travail. Crawlbase peut renvoyer une sortie propre ou markdown et du JSON auto-analysé, réduisant l'étape de nettoyage avant que pandas ne s'exécute.
Foire aux questions
Pourquoi le nettoyage des données est-il si important avant d'entraîner un modèle AI ?
Parce que les modèles apprennent ce qui est dans les données, y compris les erreurs. Les doublons sur-pondèrent certains exemples, les prix typés en chaînes ne peuvent pas être comparés, les formats de date mixtes se fragmentent en valeurs non liées, et les cellules manquantes sont mal lues par les encodeurs. Aucun de ces problèmes ne génère d'erreur, donc le pipeline s'exécute et le modèle s'entraîne silencieusement sur une vue corrompue. Le nettoyage supprime ces dommages pour que le modèle apprenne le signal plutôt que le bruit.
Dois-je dédupliquer ou gérer les valeurs manquantes en premier ?
Dédupliquer d'abord. Si vous imputez avant de supprimer les doublons, chaque moyenne avec laquelle vous remplissez est calculée sur des comptages gonflés et biaisée vers les entités les plus dupliquées. Supprimez les doublons exacts et regroupez sur une clé métier (en gardant la capture la plus récente), puis calculez les médianes et modes que vous utilisez pour l'imputation.
Comment décider entre supprimer des lignes et imputer les valeurs manquantes ?
Décidez par colonne. Supprimez la ligne quand le champ manquant est celui sans lequel l'enregistrement ne peut pas exister, comme une clé primaire ou la valeur cible, et que les lacunes sont rares. Imputez quand la colonne est nécessaire en aval et qu'une estimation défendable existe : médiane pour les numériques car elle résiste aux valeurs aberrantes, et un sentinel "unknown" explicite pour les catégorielles pour que l'absence soit enregistrée plutôt que devinée.
Quel est le nettoyage de texte minimum dont les données scrapées ont besoin ?
Désescapez les entités HTML, supprimez les balises et URLs résiduelles, et réduisez les séquences d'espaces en espaces simples, dans cet ordre, puis mettez en minuscules. Le texte scrapé contient régulièrement &, du balisage parasite et des espaces irréguliers qui deviendraient sinon des tokens bruyants. Cette passe est suffisante avant la tokenisation par espaces ; pour la NLP en production, vous remplaceriez ensuite par un tokeniseur dédié.
Pourquoi se donner la peine d'appliquer un schéma si les données sont déjà propres ?
Parce que le prochain lot ne sera pas propre sauf si quelque chose l'y force. Un schéma déclare un type par colonne et caste chaque lot vers celui-ci, de sorte qu'un scrape qui dérive (une nouvelle colonne, un prix qui ne s'analyse soudainement plus) échoue bruyamment au lieu de réintroduire silencieusement le désordre que vous venez de supprimer. Cela transforme le nettoyage d'une tâche ponctuelle en un contrat répétable.
Crawlbase peut-il réduire la quantité de nettoyage que je dois faire ?
Oui, pour la première moitié bruyante. La Crawling API peut renvoyer une vue propre en style markdown d'une page pour que le texte arrive sans balises ni texte générique, et la Scraper API analyse automatiquement de nombreux sites populaires en JSON structuré, ce qui supprime l'écriture de sélecteurs et la majeure partie des suppositions de types. Vous déduplication, validez et appliquez un schéma vous-même, car ce sont des propriétés de votre jeu de données plutôt que de la page source.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.
