Retour au blog
Guides
Andrei OgiolanLast updated on May 7, 202618 min read

Comment récupérer des tableaux HTML à l'aide de Python

Comment récupérer des tableaux HTML à l'aide de Python
En bref : la plupart des tableaux HTML peuvent être extraits à l'aide d'une seule ligne de pandas.read_html. Lorsque le tableau est paginé, rendu en JavaScript ou comporte des en-têtes fusionnés, passez à Requests + BeautifulSoup ou à un navigateur sans interface graphique comme Playwright. Ce guide vous propose une matrice de décision, du code fonctionnel pour les trois approches, ainsi que les étapes de nettoyage qui transforment les lignes extraites en données prêtes à être traitées.

Les données tabulaires sont omniprésentes sur le Web public, des infoboxes de Wikipédia et des outils d'analyse boursière aux statistiques gouvernementales, en passant par les statistiques sportives et les pages de comparaison de produits. Si vous savez extraire des tableaux HTML à l'aide de Python, vous pouvez transformer ces lignes en DataFrames propres, en documents JSON ou en lignes dans votre propre base de données en quelques minutes.

Le hic, c'est que les tableaux HTML constituent une catégorie trompeusement vaste. Certains tableaux s'inscrivent parfaitement dans un <table> un balisage que pandas peut analyser en une seule ligne. D'autres sont des grilles créées manuellement <div>, paginées sur des dizaines de pages, ou qui ne s’affichent qu’après l’exécution de JavaScript dans le navigateur. Une méthode qui fonctionne parfaitement sur Wikipédia peut renvoyer silencieusement zéro ligne sur une application monopage.

Ce guide présente trois approches Python et articule l'ensemble de l'article autour de deux questions pratiques : quelle méthode choisir, et comment faire fonctionner votre scraper lorsque le site modifie son balisage au trimestre prochain ?

Comment extraire des tableaux HTML à l'aide de Python : une matrice de décision rapide

Avant d'écrire la moindre ligne de code, déterminez quel outil convient au tableau qui se trouve devant vous. Un mauvais choix est la raison la plus courante pour laquelle les tutoriels ne résistent pas à la confrontation avec de vrais sites web. Utilisez la matrice ci-dessous pour faire votre choix.

Critère

pandas.read_html

Requests + BeautifulSoup

Playwright (ou Selenium)

Idéal lorsque

Le tableau est dans le code HTML initial et bien formé

Vous avez besoin d'un contrôle ou d'un filtrage au niveau de chaque cellule

Le tableau est généré par JavaScript

Lignes de code

~3

30 à 80

40 à 100

Vitesse par page

Rapide

Rapide

Lente (navigateur complet)

Prise en charge de JS

Non

Non

Oui

Pagination

Boucle manuelle

Boucle manuelle ou API cachée

Clic et défilement

Résilience face aux modifications de balisage

Moyenne

Élevée (vous écrivez les sélecteurs)

Élevée

Empreinte mémoire

Faible

Faible

Élevé

Trois règles empiriques :

  • Si pd.read_html(url) renvoie les lignes attendues, arrêtez-vous là. Cette ligne de code est la plus facile à maintenir que vous n'écrirez jamais.
  • Si le tableau se trouve dans le code HTML mais que vous devez filtrer, fusionner ou normaliser les cellules avant qu'elles n'atteignent un DataFrame, utilisez Requests + BeautifulSoup.
  • Si « Afficher la source de la page » affiche un <div id="grid"> et que les données n'apparaissent qu'une fois la page chargée, vous avez besoin de Playwright ou d'un point de terminaison JSON caché.

La suite de cet article explique comment extraire des tableaux HTML à l’aide de Python dans chacun de ces scénarios, ainsi que les cas particuliers qui font dérailler un code qui fonctionnerait autrement.

Anatomie d'un tableau HTML (et ce qui rend l'extraction délicate)

Un tableau HTML classique se présente ainsi :

<table id="employees" class="stripe">
  <thead><tr><th>Name</th><th>Position</th><th>Salary</th></tr></thead>
  <tbody>
    <tr><td>Ada Lovelace</td><td>Engineer</td><td>$120,000</td></tr>
    <tr><td>Alan Turing</td><td>Researcher</td><td>$135,000</td></tr>
  </tbody>
</table>

Cinq balises assurent l'essentiel du travail : <table> est le conteneur, <thead> et <tbody> regroupe les lignes, <tr> est une ligne, et <th> ou <td> sont respectivement des cellules d'en-tête et des cellules de données. Deux attributs compliquent les choses : colspan fait qu'une cellule s'étend sur plusieurs colonnes, et rowspan lui fait s'étendre sur plusieurs lignes. Ces deux éléments sont très utilisés dans les tableaux financiers et sportifs.

Dans la pratique, la moitié de ces conventions sont ignorées. De nombreuses pages omettent <thead> et <tbody>, ne mettent pas de balises de fermeture ou affichent les tableaux sous forme de <div> que nul analyseur syntaxique ne reconnaîtra comme un tableau. Le scraping en conditions réelles consiste principalement à faire face à cette dérive, c'est pourquoi pandas seul ne suffit pas sur tous les sites.

Méthode 1 : pandas.read_html, la solution en une ligne

pandas.read_html est une fonction pratique de la bibliothèque de manipulation de données pandas qui prend une URL ou une chaîne HTML et renvoie une liste de DataFrames, un par <table> qu'elle peut trouver. Selon la documentation de pandas, elle nécessite soit lxml, html5lib, ou bs4 en arrière-plan, et identifie les tableaux en recherchant des éléments de tableau standard.

Tout l'intérêt réside dans le fait que vous pouvez écrire trois lignes de code et obtenir un DataFrame typé et interrogeable :

import pandas as pd

tables = pd.read_html("https://en.wikipedia.org/wiki/List_of_largest_companies_by_revenue")
df = tables[0]
print(df.head())

Le hic, c'est que read_html ne voit que ce qui se trouve déjà dans le corps de la réponse. Si le tableau est rempli par JavaScript après le chargement de la page, la fonction lève une exception ValueError: No tables found même si le tableau est clairement visible dans votre navigateur. Connaître cette limitation dès le départ vous évite bien des efforts de débogage.

Configurer votre environnement Python

Vous pouvez exécuter tous les exemples de ce guide avec un nouvel environnement virtuel et trois paquets :

python -m venv .venv
source .venv/bin/activate
pip install pandas requests beautifulsoup4 lxml html5lib playwright
playwright install chromium

lxml est l'analyseur HTML le plus rapide disponible pour Python et celui que la plupart des professionnels utilisent par défaut. html5lib est plus lent mais suit l'algorithme d'analyse WHATWG, ce qui en fait le choix le plus tolérant en cas de balisage incorrect. Installez les deux afin de pouvoir changer d'analyseur si l'un d'eux rencontre des difficultés.

Guide complet de pandas.read_html

Extraitons un tableau réel et bien formé : la liste des pays classés par PIB sur Wikipédia. Le workflow complet tient en quatre lignes.

import pandas as pd

url = "https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)"
tables = pd.read_html(url)
print(f"Found {len(tables)} tables on the page")

gdp = tables[2]            # pick the right one by index
gdp.columns = [c[1] if isinstance(c, tuple) else c for c in gdp.columns]
print(gdp.head())

Trois points à noter. Premièrement, read_html renvoie une liste, vous devez donc l'indexer. Deuxièmement, les tableaux de Wikipédia comportent souvent des en-têtes à plusieurs niveaux, que pandas expose sous la forme d’un MultiIndex. La compréhension de liste l'aplatit en conservant le niveau inférieur. Troisièmement, aucune itération manuelle des lignes : chaque cellule se trouve déjà dans une colonne typée que vous pouvez appeler .sort_values, .groupby, ou .to_csv .

Lorsque vous n'avez besoin des données que pour une analyse rapide, c'est véritablement tout le code que vous devriez écrire.

Dépannage de pandas.read_html : erreurs courantes

pd.read_html échoue de manière prévisible. Mémorisez ces quatre points et vous résoudrez la plupart des problèmes en moins d’une minute.

  1. ValueError: No tables found. La page est soit rendue en JavaScript, soit protégée par une page de connexion. Passez directement à la section Playwright.
  2. Code HTTP 403 ou 429 renvoyé par le récupérateur interne de pandas. L' urllib est bloqué. Récupérez le code HTML vous-même avec Requests et transmettez la chaîne à read_html:
import requests, pandas as pd
headers = {"User-Agent": "Mozilla/5.0 (compatible; analytics-bot/1.0)"}
html = requests.get(url, headers=headers, timeout=15).text
tables = pd.read_html(html)
  1. Index de table incorrect. Utilisez match= pour filtrer par une chaîne apparaissant dans la table cible, par exemple pd.read_html(html, match="Population"). Cette méthode est bien plus stable que de se fier à tables[3].
  2. Caractères illisibles dans le contenu non ASCII. Forcez l'encodage en lisant explicitement les octets : response = requests.get(url); response.encoding = "utf-8"; tables = pd.read_html(response.text).

Si vous rencontrez toujours des difficultés après ces corrections, le tableau nécessite très certainement Requests + BeautifulSoup ou un navigateur sans interface graphique, et non pas d'autres read_html solutions de contournement.

Méthode 2 : Requests + BeautifulSoup, lorsque vous avez besoin de contrôle

pandas.read_html est idéal lorsque vous souhaitez que chaque cellule corresponde exactement à ce qui apparaît dans le code HTML. Dès que vous devez filtrer des lignes pendant l'extraction, joindre des valeurs de deux colonnes, supprimer les symboles monétaires à la volée ou extraire href d'une cellule liée, ce n'est plus l'outil adéquat.

C'est là que Requests + BeautifulSoup entre en jeu. Requests gère la couche HTTP (en-têtes, cookies, sessions, tentatives de reconnexion), et BeautifulSoup vous fournit un arbre de parse que vous pouvez parcourir à l'aide de sélecteurs CSS, de la correspondance d'attributs ou de la navigation entre éléments frères. Si vous débutez avec BeautifulSoup, notre guide approfondi sur l'extraction et l'analyse de données Web avec Python et BeautifulSoup vous présente l'API en détail. Cette combinaison est également celle que la plupart des scrapers de production finissent par adopter, car chaque étape (récupération, analyse, extraction, transformation) est sous votre contrôle.

Les trois sections suivantes montrent comment extraire des tableaux HTML à l'aide de Python avec cette pile : une requête polie, un sélecteur robuste pour le tableau et une boucle de lignes qui ne se casse pas lorsqu'une colonne est ajoutée.

Envoi de requêtes HTTP courtoises et réalistes

Les défenses anti-bot s’appuient sur quelques signaux simples : un User-Agent manquant ou par défaut, l’absence de Accept-Language, pas de cookies et un trafic qui épuise une session en une seconde. Imitez un vrai navigateur et réutilisez une connexion :

import requests
from bs4 import BeautifulSoup

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
})

response = session.get("https://example.com/employees", timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "lxml")

Trois petites habitudes font la différence. Session() conserve les cookies et le pool de connexions entre les appels. raise_for_status() transforme les réponses 4xx/5xx silencieuses en exceptions que vous pouvez réessayer. Et le fait de passer "lxml" en tant qu'analyseur est environ cinq à dix fois plus rapide que l'analyseur intégré html.parser sur les pages volumineuses.

Localiser le bon tableau sur la page

Une fois que vous disposez d’un BeautifulSoup objet, le problème suivant consiste à récupérer la bonne <table>. Les pages en contiennent généralement entre huit et quinze (pensez aux tables de mise en page, aux widgets de la barre latérale, aux contrôles de pagination masqués). Essayez les sélecteurs dans cet ordre de priorité :

# 1. By stable id (best)
table = soup.find("table", id="employees")

# 2. By a class that's specific to this table
table = soup.find("table", class_="data-grid")

# 3. By a CSS selector
table = soup.select_one("section#payroll table.stripe")

# 4. By the heading that precedes it (when classes are dynamic)
heading = soup.find(["h2", "h3"], string=lambda s: s and "Employees" in s)
table = heading.find_next("table") if heading else None

Lorsque les noms de classe sont générés automatiquement et changent à chaque déploiement (un modèle courant dans React), privilégiez XPath via lxml, car il permet d'exprimer « la troisième table à l'intérieur de la section dont le texte d'en-tête contient « Employés » » en une seule expression. Nous proposons un guide distinct sur les sélecteurs XPath par rapport aux sélecteurs CSS qui approfondit ce compromis.

Itérer sur les lignes et extraire les cellules en toute sécurité

La plupart des tutoriels de scraping montrent des boucles de lignes qui indexent les cellules par position : cells[0] est le nom, cells[1] est la position, cells[2] est le salaire. Ce code cesse de fonctionner dès que quelqu’un ajoute une colonne « Département ». La méthode robuste consiste à lire les en-têtes une seule fois et à les associer à chaque ligne.

# Read headers from <thead> if present, else from the first row
header_cells = table.select("thead th") or table.select("tr:first-of-type th, tr:first-of-type td")
headers = [th.get_text(strip=True) for th in header_cells]

rows = []
for tr in table.select("tbody tr") or table.select("tr")[1:]:
    cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
    if not cells:
        continue
    rows.append(dict(zip(headers, cells)))

print(f"Extracted {len(rows)} rows with {len(headers)} columns")

Cela vous offre trois avantages gratuits. Les nouvelles colonnes sont automatiquement prises en compte car les clés proviennent des en-têtes, et non des indices. Les lignes vides (souvent utilisées comme séparateurs visuels) sont ignorées. Et chaque cellule passe par get_text(strip=True), ce qui réduit les espaces et supprime les \n caractères qui hantent les appels naïfs cell.text . Voici la boucle de lignes que vous devriez copier dans chaque projet BeautifulSoup.

Enregistrement des lignes extraites au format JSON, CSV ou Parquet

Une fois que vous disposez d’une liste de dictionnaires, leur persistance se résume à une seule ligne par format :

import json
import pandas as pd

# JSON, human-readable, UTF-8 safe
with open("employees.json", "w", encoding="utf-8") as f:
    json.dump(rows, f, indent=2, ensure_ascii=False)

# CSV via pandas (handles quoting, encoding, and missing keys)
df = pd.DataFrame(rows)
df.to_csv("employees.csv", index=False, encoding="utf-8")

# Parquet for analytical pipelines (smaller files, typed columns)
df.to_parquet("employees.parquet", index=False)

Optez pour JSON lorsque l'utilisateur est un script ou une interface utilisateur, pour CSV lorsqu'un humain va l'ouvrir dans Excel ou BigQuery, et pour Parquet lorsque l'ensemble de données dépasse quelques centaines de milliers de lignes ou alimente Spark, Snowflake ou DuckDB. Les fichiers Parquet sont généralement 5 à 10 fois plus petits que les CSV équivalents et conservent les types de données. Pour tout ce qui est destiné à une base de données relationnelle, passez directement à df.to_sql afin d'éviter complètement le fichier intermédiaire.

Gestion des en-têtes complexes avec colspan et rowspan

Les en-têtes sur deux lignes sont courants dans la finance, les statistiques gouvernementales et les tableaux sportifs. La ligne supérieure regroupe les colonnes (« T1 2024 », « T2 2024 »), et la ligne inférieure les étiquette (« Chiffre d'affaires », « Bénéfice »). Le codage en dur des noms de colonnes comme ["Name", "Position", "Contact"] fonctionne une fois, puis ne fonctionne plus jamais. Voici un algorithme générique qui respecte colspan et rowspan.

def expand_header(table):
    # Return a flat list of column labels from a multi-row <thead>
    rows = table.select("thead tr")
    if not rows:
        return [th.get_text(strip=True) for th in table.select("tr:first-of-type th")]

    grid = []  # grid[row_index] = list of column labels at that row
    for r, tr in enumerate(rows):
        while len(grid) <= r:
            grid.append([])
        col = 0
        for th in tr.find_all(["th", "td"]):
            # skip already-filled slots from previous rowspans
            while col < len(grid[r]) and grid[r][col] is not None:
                col += 1
            text = th.get_text(strip=True)
            colspan = int(th.get("colspan", 1))
            rowspan = int(th.get("rowspan", 1))
            for dr in range(rowspan):
                while len(grid) <= r + dr:
                    grid.append([])
                row_buf = grid[r + dr]
                # pad
                while len(row_buf) < col + colspan:
                    row_buf.append(None)
                for dc in range(colspan):
                    row_buf[col + dc] = text
            col += colspan

    # Combine the columns of each row, top-down, into a single label per column
    n_cols = max(len(r) for r in grid)
    flat = []
    for c in range(n_cols):
        parts = [grid[r][c] for r in range(len(grid)) if c < len(grid[r]) and grid[r][c]]
        # de-dup adjacent identical strings: ['Q1 2024', 'Q1 2024', 'Revenue'] -> 'Q1 2024 Revenue'
        seen = []
        for p in parts:
            if not seen or seen[-1] != p:
                seen.append(p)
        flat.append(" ".join(seen))
    return flat

Associez cela à la même zip(headers, cells) boucle de lignes utilisée précédemment, et vous obtenez un analyseur d’en-têtes capable de gérer n’importe quelle combinaison de cellules fusionnées. La même idée (une grille 2D que vous remplissez colspan par colspan) s’étend au corps lorsque les rowspans répètent des valeurs sur plusieurs colonnes : suivez les emplacements déjà occupés et ignorez-les lors des <tr> .

Extraction de données de tableaux HTML paginés (trois stratégies)

La pagination est l’aspect le plus sous-estimé du scraping de tableaux HTML avec Python. La plupart des tutoriels se contentent de montrer « cliquez sur le bouton Suivant dans un navigateur sans interface graphique », ce qui est l’approche la plus lente et la plus fragile. Essayez d’abord ces trois méthodes, par ordre de préférence.

1. Augmentez le paramètre de requête page-size. De nombreux tableaux acceptent ?per_page=500 ou ?length=1000. Une seule requête, toutes les lignes, pas de boucle. Inspectez l'URL lorsque vous cliquez sur le menu déroulant de la taille de la page et vous trouverez souvent cette option gratuitement.

2. Accédez à l'API JSON sous-jacente. Ouvrez DevTools, passez à l'onglet Réseau, filtrez par Fetch/XHR, puis cliquez sur la page suivante. Presque tous les tableaux de données modernes s’appuient sur un point de terminaison qui renvoie du JSON. L’appeler directement permet d’éviter complètement l’analyse HTML :

import requests
url = "https://example.com/api/employees"
all_rows = []
for page in range(1, 20):
    payload = requests.get(url, params={"page": page, "size": 100}, timeout=15).json()
    if not payload["items"]:
        break
    all_rows.extend(payload["items"])

3. Parcourez les chaînes de requête de page. Lorsque l'URL contient le numéro de page (?page=2, &start=20), itérez explicitement et arrêtez-vous lorsque le tableau revient vide. Cette méthode est plus fiable que l'utilisation d'un navigateur, car il n'y a rien à cliquer et aucune animation à attendre.

Un navigateur sans interface graphique est votre dernier recours, pas votre premier. Réservez-le aux tableaux où le lien vers la page suivante est lié à un gestionnaire JavaScript sans changement d'URL.

Méthode 3 : Playwright pour les tableaux rendus en JavaScript

Lorsque le tableau n'apparaît qu'après le chargement de la page, vous avez besoin d'un outil capable d'exécuter du JavaScript. Playwright est le choix moderne : il fournit des liaisons Python officielles, exécute Chromium, Firefox ou WebKit, et dispose d'un comportement d'attente automatique fiable. Voici le modèle complet pour extraire des tableaux HTML à l'aide de Python qui dépendent de JS :

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
import pandas as pd

URL = "https://example.com/dashboard"

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(user_agent="Mozilla/5.0 ... Chrome/124.0 Safari/537.36")
    page.goto(URL, wait_until="domcontentloaded")

    # Wait for the actual data, not just the page load
    page.wait_for_selector("table#grid tbody tr", timeout=15000)

    html = page.content()
    browser.close()

# Hand the rendered HTML off to your existing parser
soup = BeautifulSoup(html, "lxml")
table = soup.find("table", id="grid")
# ... use the same row loop from earlier ...

# Or, when the table is well-formed, skip BeautifulSoup entirely:
df = pd.read_html(html, match="Department")[0]
print(df.head())

Le schéma est toujours le même : naviguer, attendre les données (pas seulement load), récupérer page.content(), puis insérer cette chaîne dans le même code d'analyse que vous utiliseriez pour du HTML statique. Reportez-vous à la documentation de Playwright pour Python pour l'installation, les API asynchrones et le traçage.

Selenium et Pyppeteer constituent des alternatives valables. Selenium dispose d’un écosystème plus vaste et constitue un choix sûr si votre équipe l’utilise déjà pour des tests de bout en bout ; notre tutoriel étape par étape sur Selenium couvre la configuration équivalente. Pyppeteer est plus léger mais moins activement maintenu. Pour une comparaison plus complète des outils headless, consultez notre guide de scraping Web avec Playwright. Pour les nouveaux projets, Playwright s’avère généralement le plus ergonomique.

Choisir un parseur HTML et gérer les cellules vides

BeautifulSoup est un wrapper. L'analyse proprement dite est déléguée à l'un des trois backends, et ce choix a plus d'importance que ne l'admettent la plupart des tutoriels.

Analyseur

Vitesse

Tolérance vis-à-vis du code HTML incorrect

Installation

html.parser

Lente

Moyenne (intégré à Python)

Aucune

lxml

Rapide

Assez strict, mais pragmatique

pip install lxml

html5lib

Le plus lent

Le plus élevé, conforme au WHATWG

pip install html5lib

Par défaut lxml. Passer à html5lib uniquement lorsque lxml renvoie un arbre partiel sur une page dont le balisage est incorrect (balise de fermeture manquante </td>, non fermé <tr>, des < ). Vous pouvez vérifier rapidement :

import time
from bs4 import BeautifulSoup

for parser in ["lxml", "html.parser", "html5lib"]:
    t0 = time.perf_counter()
    soup = BeautifulSoup(html, parser)
    rows = soup.select("tbody tr")
    print(f"{parser:10} {len(rows):4} rows in {time.perf_counter()-t0:.3f}s")

Pour les cellules vides, écrivez une fonction d'aide qui renvoie une valeur par défaut pertinente plutôt que de provoquer un plantage :

def cell_text(cell, default=""):
    if cell is None:
        return default
    text = cell.get_text(" ", strip=True)
    return text if text else default

Utilisez-la partout où vous indexez une ligne. None Les vérifications à chaque point d'appel encombrent la boucle et ne détectent pas le cas où la cellule existe mais ne contient que &nbsp;. Cette fonction d'aide gère les deux cas.

Éviter les blocages : en-têtes, sessions et proxys

Un code de statut 200 signifie que la requête a été acceptée. Tout autre code (en particulier 403, 429 ou 503) signifie généralement que le site a détecté votre scraper. Parcourez cette échelle dans l'ordre, en vous arrêtant au premier échelon qui fonctionne.

  1. En-têtes réalistes. Définissez User-Agent, Accept-Languageet Referer sur des valeurs qu'une véritable session Chrome enverrait. Cela permet à lui seul de contourner un nombre surprenant de blocages.
  2. Sessions persistantes. Utilisez requests.Session() so pour que les cookies définis par la page d'accueil soient envoyés lors des appels suivants. De nombreux sites émettent un cookie de session lors de la première visite et rejettent les requêtes qui n'en disposent pas.
  3. Délai d'attente exponentiel pour les codes 429 et 503. Attendez 2 ** attempt secondes et réessayer jusqu'à cinq fois. Respecter les Retry-After les en-têtes lorsque le serveur les fournit.
  4. Proxys de centres de données. Bon marché, rapides et suffisants pour la plupart des sites statiques. Faites tourner les adresses IP au sein de votre pool de workers.
  5. Proxys résidentiels. De véritables adresses IP résidentielles provenant de 195 pays, utilisées lorsque les plages de centres de données sont déjà bloquées. Plus lents mais plus difficiles à détecter.
  6. API de scraping gérées. Lorsque vous souhaitez vous concentrer sur l'analyse plutôt que sur l'infrastructure, des services comme notre API Scraper chez WebScrapingAPI gèrent la rotation des proxys, la génération d'en-têtes et les tentatives de reconnexion derrière un point de terminaison unique, afin que le même code BeautifulSoup ou pandas continue de fonctionner.

La plupart des projets ont besoin des étapes 1 à 3. Pour une liste plus complète des signaux de détection, notre guide expliquant pourquoi les scrapers sont bloqués ou bannis par IP aborde en détail l'empreinte TLS, l'ordre des en-têtes et les calculs de limitation de débit. Si vous êtes bloqué sur un article Wikipédia, c'est qu'il y a un autre problème.

Nettoyage, coercition de types et exportation pour la production

Les tableaux extraits ne sont presque jamais prêts pour l'analyse. Les symboles monétaires, les signes de pourcentage, les marqueurs de notes de bas de page et les espaces finaux s'y glissent tous sous forme de chaînes de caractères. Corrigez-les en une seule passe avant d'enregistrer :

import pandas as pd

df = pd.DataFrame(rows)

# 1. Strip whitespace on every text column
str_cols = df.select_dtypes(include="object").columns
df[str_cols] = df[str_cols].apply(lambda s: s.str.strip())

# 2. Coerce numeric columns (errors='coerce' turns junk into NaN)
df["salary"] = pd.to_numeric(df["salary"].str.replace(r"[^0-9.\-]", "", regex=True),
                             errors="coerce")
df["growth_pct"] = pd.to_numeric(df["growth_pct"].str.rstrip("%"), errors="coerce")

# 3. Coerce dates
df["hired_at"] = pd.to_datetime(df["hired_at"], errors="coerce")

# 4. Drop rows where the primary key failed to parse
df = df.dropna(subset=["employee_id"])

# 5. Persist
df.to_parquet("employees.parquet", index=False)
df.to_sql("employees", con=engine, if_exists="replace", index=False)

Le errors="coerce" drapeau est le héros méconnu de ce pipeline : les cellules erronées deviennent NaN au lieu de déclencher une exception, et vous pouvez les examiner plus tard avec df[df["salary"].isna()]. Pour les pipelines de production, écrivez en Parquet pour le stockage et utilisez to_sql pour stocker les données nettoyées dans Postgres ou l'entrepôt de votre choix.

Contrôles juridiques et éthiques

Il s'agit de conseils visant à réduire les risques, et non de conseils juridiques. Consultez un avocat avant de scraper toute information sensible.

  • Lisez le fichier robots.txt. Il exprime la préférence du propriétaire du site, et non une règle juridique, mais l'ignorer est le moyen le plus rapide de se faire bloquer. La spécification est documentée dans la RFC 9309.
  • Lisez les conditions d'utilisation. Le scraping après connexion, en particulier, enfreint souvent les conditions d'utilisation, même lorsque le fichier robots.txt ne s'y oppose pas.
  • Limitez vous-même votre débit. Une requête par seconde est une valeur par défaut raisonnable pour les petits projets. Ajoutez un jitter pour ne pas ressembler à une horloge.
  • Évitez les données personnelles, sauf si vous disposez d'une base légale. Le RGPD et les lois similaires s'appliquent même lorsque les données sont techniquement publiques.
  • Citez vos sources lors de la republication. Indiquez l'URL source et la date de scraping.

Savoir scraper des tableaux HTML avec Python relève pour moitié de la technique et pour moitié de l'éthique. La partie technique peut échouer une fois ; la partie éthique peut ruiner votre entreprise.

Points clés

  • Choisissez l'outil le plus simple qui fonctionne. pandas.read_html Pour les tableaux statiques propres, utilisez Requests + BeautifulSoup ; pour le contrôle, utilisez Playwright pour les tableaux rendus en JS ou pilotés par des interactions.
  • Des en-têtes, pas des index. Compressez le texte des en-têtes avec celui des cellules pour que votre scraper résiste à l'ajout d'une colonne. Le code en dur cells[0], cells[1] est une dette technique.
  • La pagination comporte trois niveaux. Essayez per_page=500, puis une API JSON cachée, puis des boucles de numéros de page. Un navigateur sans interface graphique est le dernier recours.
  • Nettoyez avant d'enregistrer. pd.to_numeric, pd.to_datetime, et errors="coerce" transformez les lignes scrapées en un DataFrame typé prêt pour l'analyse.
  • Respectez le site. Respectez le fichier robots.txt, limitez le nombre de requêtes et évitez les données personnelles, sauf si vous disposez d'une base légale claire.

FAQ

Quelle est la différence entre pandas.read_html et BeautifulSoup pour le scraping de tableaux ?

pandas.read_html est un raccourci de haut niveau : il renvoie directement des DataFrames mais ne gère que les tableaux déjà présents dans la réponse HTML. BeautifulSoup est un analyseur HTML de bas niveau qui vous donne un contrôle total sur les cellules que vous conservez, la manière dont vous les transformez et la façon de naviguer dans un balisage non standard. Utilisez read_html pour des données prêtes à l'analyse, et de BeautifulSoup lorsque les règles dont vous avez besoin ne peuvent pas être exprimées sous la forme « donne-moi le tableau N ».

Comment extraire un tableau HTML qui n'apparaît qu'après l'exécution de JavaScript ?

Commencez par vérifier qu’il s’agit bien d’un rendu JavaScript : affichez le code source de la page (Ctrl+U), recherchez un mot du tableau, et s’il est absent, le tableau est généré côté client. La solution la plus rapide consiste à trouver le point de terminaison JSON sous-jacent dans l’onglet Réseau de DevTools et à l’appeler directement. Si cela n’est pas possible, utilisez un navigateur sans interface graphique comme Playwright, attendez un sélecteur de ligne, puis transmettez page.content() à votre parseur habituel.

Que faire lorsqu'un tableau comporte des cellules fusionnées (rowspan ou colspan) ?

Traitez le tableau comme une grille 2D que vous remplissez cellule par cellule, en respectant colspan et rowspan , plutôt que comme une liste de lignes. Pour chaque <th> ou <td>, répétez sa valeur dans les emplacements couverts par son étendue, et ignorez les emplacements déjà remplis par un rowspan antérieur. Cela produit une matrice rectangulaire que vous pouvez transmettre à pd.DataFrame sans décalage dans le nombre de colonnes.

Comment conserver le type correct des colonnes numériques et de date après avoir extrait les données d'un tableau ?

Supprimez les caractères non numériques à l'aide d'une expression régulière (str.replace(r"[^0-9.\-]", "", regex=True)), puis appelez pd.to_numeric(series, errors="coerce") pour que les valeurs non analysables deviennent NaN au lieu de déclencher une exception. Pour les dates, pd.to_datetime(series, errors="coerce", format="%Y-%m-%d") est l'équivalent. L'ajout de l' format rend l'analyse environ 10 fois plus rapide sur les colonnes volumineuses et évite les faux positifs dus à des chaînes ambiguës.

Puis-je exécuter pandas.read_html sur un fichier HTML local ou une chaîne HTML brute ?

Oui. pd.read_html accepte une URL, un chemin vers un fichier local ou une chaîne HTML brute. Passez pd.read_html(open("page.html").read()) pour lui fournir une chaîne de caractères, ou pd.read_html("page.html") pour un chemin de fichier. Cela est utile pour les tests unitaires (commiter un fixture HTML dont la validité est confirmée) et pour séparer la récupération des données de l'analyse dans les scrapers de production.

Conclusion

Savoir comment extraire des tableaux HTML à l'aide de Python consiste principalement à adapter l'outil au tableau. Commencez par pandas.read_html , passez ensuite à Requests + BeautifulSoup lorsque vous avez besoin d’un contrôle au niveau des cellules, et n’utilisez Playwright que lorsque JavaScript rend les données. Ajoutez-y des boucles de lignes tenant compte des en-têtes, un analyseur générique colspan/rowspan, une pagination intelligente et un passage de nettoyage avec pandas, et vous obtiendrez un scraper capable de survivre aux modifications de balisage au lieu de tomber en panne lors du prochain déploiement.

Lorsque vous dépassez les limites de la rotation de proxys et du rendu JavaScript faits maison, WebScrapingAPI propose une API de scraping qui gère la couche de requêtes derrière un point de terminaison unique, afin que votre code d'analyse continue de fonctionner. À partir de là, consultez nos guides plus approfondis sur les tableaux JavaScript et sur la manière d'éviter les blocages.

À propos de l'auteur
Andrei Ogiolan, Développeur Full Stack @ WebScrapingAPI
Andrei OgiolanDéveloppeur Full Stack

Andrei Ogiolan est développeur Full Stack chez WebScrapingAPI ; il participe à l'ensemble du produit et contribue à la mise au point d'outils et de fonctionnalités fiables pour la plateforme.

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.