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

Web Scraping JavaScript Tables in Python : Des API cachées à Playwright

Web Scraping JavaScript Tables in Python : Des API cachées à Playwright
En bref : pour extraire des tableaux JavaScript via le web scraping en Python, on a rarement besoin d'un navigateur sans interface graphique. Ouvrez les DevTools, identifiez le point de terminaison JSON qui alimente la grille, relancez-le avec requests, paginez-le, et ne recourez à Playwright que lorsque l'appel réseau est signé, chiffré ou autrement verrouillé.

Vous avez écrit le code évident. requests.get(url), transmettez le HTML à BeautifulSoup, extrayez les lignes du <table>. Le script s'exécute, le fichier atterrit sur le disque, et le CSV est vide. Bienvenue dans le monde du web scraping des tableaux JavaScript, où les lignes que vous voyez dans votre navigateur n'existent pas dans le document que le serveur a réellement renvoyé.

Les tableaux statiques transmettent les données dans le code HTML initial. Les tableaux dynamiques (également appelés tableaux AJAX ou rendus par JavaScript) transmettent une coquille presque vide, puis un script dans la page appelle un point de terminaison JSON et injecte des lignes dans le DOM après le chargement. Si vous n’exécutez pas ce script, vous ne voyez pas ces lignes. Lancer un navigateur complet pour résoudre ce problème est une solution lourde pour ce qui est généralement un petit problème.

Ce guide emprunte la voie la plus courte. Nous commencerons par un arbre de décision afin que vous n'ayez plus à vous demander s'il faut recourir à requests ou un moteur de navigateur, puis nous vous guiderons pour trouver le point de terminaison JSON sous-jacent dans DevTools, le reproduire en Python avec pagination et authentification, le parser en lignes propres, et l'exporter au format CSV, JSON Lines ou SQLite. Playwright est ici une véritable solution de secours pour les sites qui masquent l'appel réseau, et non l'outil par défaut. À la fin, vous disposerez d'un script que vous pourrez réexécuter au trimestre prochain sans avoir à le réécrire de zéro.

Pourquoi les tableaux JavaScript mettent à mal les scrapers standard

Lorsque vous appelez requests.get() sur une page contenant un tableau JavaScript, ce qui vous est renvoyé est le document que le serveur a envoyé avant l'exécution de tout code du navigateur. Ce document contient la mise en page, la navigation, le conteneur de grille vide et un ensemble de code JavaScript. Les lignes ne sont pas encore présentes. Le navigateur exécute le script, le script récupère une charge utile JSON, et ce n'est qu'alors que le tableau est rempli.

BeautifulSoup analyse fidèlement ce qui lui est fourni, à savoir un <table> sans <tr> enfants. Votre sélecteur ne correspond à rien, votre boucle s'exécute zéro fois, et l'éditeur produit un fichier CSV avec des en-têtes mais sans données. Le scraping de tables JavaScript échoue ici, silencieusement, car techniquement, chaque étape a fonctionné.

Choisissez un chemin d'extraction avant d'écrire du code

Avant d'ouvrir un éditeur, passez par une échelle de décision d'une minute. Le classement est important car chaque étape coûte plus cher à maintenir que celle qui la précède.

  1. API officielle ou export CSV. De nombreux tableaux de bord proposent un bouton de téléchargement ou un point de terminaison documenté. Utilisez-le. Vous n’allez pas extraire ce que vous pouvez simplement demander à l’aide d’une clé.
  2. XHR caché ou JSON Fetch. La plupart des grilles modernes sont alimentées par un appel JSON que vous pouvez voir dans DevTools. Cela devrait être votre option par défaut pour le web scraping des tableaux JavaScript. La charge utile est structurée, le schéma est stable et vous contournez toute la couche de rendu.
  3. Statique <table> déjà dans la source. Si les lignes sont présentes dans view-source: (aucun script nécessaire), analysez le HTML avec pandas.read_html() pour un résultat rapide ou requests avec BeautifulSoup et lxml pour la production.
  4. Rendu via un navigateur headless. N'utilisez Playwright que lorsque le chemin réseau est signé, en GraphQL avec des vérifications d'origine strictes, alimenté par WebSocket, ou autrement inaccessible depuis un simple client HTTP.

La plupart des articles enseignent d'abord la méthode 4. C'est l'inverse. Un point de terminaison JSON caché, lorsqu'il existe, vous offre des données plus propres et une surface d'échec plus réduite que n'importe quel navigateur sans interface utilisateur ne le fera jamais.

Localisez le point de terminaison JSON caché avec DevTools

Le moyen le plus rapide de confirmer qu'un tableau est alimenté par JavaScript est de vérifier le code source brut de la page, et non le DOM rendu. Cliquez avec le bouton droit sur la page, choisissez « Afficher la source », puis recherchez une valeur visible dans le tableau (un nom, un salaire, un identifiant unique). Si la recherche ne donne aucun résultat, la ligne a été injectée après le chargement et vous êtes face à une grille rendue par JavaScript.

Recherchez maintenant la requête qui a fourni les données. L'exemple de référence utilisé tout au long de ce guide est la démo AJAX publique de DataTables disponible à l'adresse datatables.net/examples/data_sources/ajax.html. Ouvrez DevTools, passez à l'onglet Réseau et filtrez par Fetch/XHR. Rechargez la page afin de capturer l'intégralité du trafic, puis déclenchez un tri ou un changement de pagination. C'est cette deuxième action qui est la clé : la charge utile la plus importante après un changement de tri est presque toujours celle qui transporte les lignes.

Cliquez sur l'appel, ouvrez la réponse et vérifiez que le format JSON correspond à vos attentes. Vérifiez également les en-têtes pour la méthode de requête, les paramètres de requête, les cookies et tout jeton personnalisé (X-CSRF-Token, Authorization). Pour les cibles complexes, cliquez avec le bouton droit sur la requête et sélectionnez « Copier en tant que cURL ». Cela préserve les en-têtes, les cookies et le corps exact, ce qui vous permet de le coller dans un convertisseur et de démarrer votre code Python sans rien taper à la main. Filtrez de manière rigoureuse : une simple saisie dans un champ de recherche peut déclencher dix requêtes d'autocomplétion avant la requête réelle.

Reproduisez la requête capturée en Python

Une fois que vous disposez de l'URL et des en-têtes, le code Python est succinct. Commencez par le strict minimum et n'ajoutez des en-têtes que lorsque le serveur signale une erreur.

import requests

URL = "https://datatables.net/examples/ajax/data/objects.txt"

headers = {
    "User-Agent": "Mozilla/5.0 (compatible; tables-scraper/1.0)",
    "Accept": "application/json, text/javascript, */*; q=0.01",
}

response = requests.get(URL, headers=headers, timeout=15)
response.raise_for_status()
payload = response.json()

Deux points à souligner. Premièrement, raise_for_status() est incontournable car les systèmes anti-bot renvoient souvent du HTML avec un statut HTTP 200, et une vérification de statut manquante transforme un blocage léger en données corrompues. Deuxièmement, résistez à l'envie de coller votre cookie de session personnel depuis DevTools. Ce cookie expire, divulgue des informations personnelles dans votre dépôt et lie le script à un seul utilisateur. Privilégiez les en-têtes publics, puis ajoutez un véritable flux de connexion avec un requests.Session si le point de terminaison nécessite réellement une authentification.

Pour les workflows nécessitant une diffusion asynchrone vers de nombreux points de terminaison, HTTPX est une alternative prête à l'emploi avec une API synchrone quasi identique et une prise en charge asynchrone de premier ordre. Considérez cela comme une option plutôt que comme une recommandation stricte ; requests reste un choix par défaut tout à fait valable en 2026.

Parsez la charge utile JSON en lignes propres

L'exemple DataTables renvoie un dictionnaire de niveau supérieur avec une data clé contenant une liste de listes. Les API réelles varient : certaines renvoient une liste d'objets, d'autres regroupent les lignes sous results ou items, d’autres les enfouissent à deux niveaux de profondeur sous payload.table.rows. Inspectez la structure une fois, puis écrivez du code défensif.

rows = payload.get("data", [])
records = []
for r in rows:
    records.append({
        "name":       r[0],
        "position":   r[1],
        "office":     r[2],
        "extn":       r[3],
        "start_date": r[4],
        "salary":     r[5],
    })

Si le point de terminaison renvoie une liste d'objets au lieu de tableaux positionnels, remplacez les indices par r.get("name"), r.get("position"), et ainsi de suite. Utilisez .get() au lieu de r["name"] vous évite de devoir KeyError le jour où le backend ajoute ou renomme un champ. Effectuez ce mappage une seule fois, à un seul endroit, afin que le reste du pipeline communique avec un schéma interne stable plutôt qu’avec ce que l’API en amont a décidé de fournir cette semaine.

Gérer la pagination, les paramètres de requête et l'authentification

Les véritables points de terminaison vous fournissent rarement toutes les lignes en un seul appel. Le protocole côté serveur de DataTables utilise draw, start, length, order[0][column], et search[value]; la liste canonique des paramètres se trouve dans le manuel de traitement côté serveur de DataTables. D'autres backends utilisent la pagination par curseur (?cursor=eyJ...), la pagination par décalage (?page=3&per_page=100) ou un next_url champ intégré à la réponse.

import time

session = requests.Session()
session.headers.update(headers)

start, length, rows = 0, 100, []
while True:
    r = session.get(URL, params={"draw": 1, "start": start, "length": length}, timeout=15)
    if r.status_code == 429:
        time.sleep(2 ** (start // length))  # crude exponential backoff
        continue
    r.raise_for_status()
    page = r.json().get("data", [])
    if not page:
        break
    rows.extend(page)
    start += length

Si le point de terminaison est protégé par une authentification, effectuez d'abord la connexion avec session.post() et laissez le cookie jar gérer la session. Pour les requêtes POST protégées contre les attaques CSRF, récupérez le jeton à partir d'un champ masqué ou d'un XSRF-TOKEN cookie et transmettez-le en en-tête. Ne collez jamais une chaîne de cookie statique. Elle expire du jour au lendemain et interrompt toutes les exécutions cron suivantes.

Exportez les lignes au format CSV, JSON Lines ou SQLite

Choisissez le format de sortie que vos outils en aval utilisent réellement. Le CSV convient pour les feuilles de calcul, JSON Lines est plus adapté à l'ingestion en continu et aux pipelines LLM ou RAG, et SQLite est l'option la plus légère et la plus conviviale pour les analystes, qui résiste à un redémarrage.

import csv, json, sqlite3

# CSV with named headers (clearer than raw csv.writer)
with open("rows.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=records[0].keys())
    writer.writeheader()
    writer.writerows(records)

# JSON Lines
with open("rows.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(r, ensure_ascii=False) + "\n")

# SQLite
con = sqlite3.connect("rows.db")
con.execute("CREATE TABLE IF NOT EXISTS staff (name TEXT, position TEXT, office TEXT, extn TEXT, start_date TEXT, salary TEXT)")
con.executemany("INSERT INTO staff VALUES (:name, :position, :office, :extn, :start_date, :salary)", records)
con.commit(); con.close()

csv.DictWriter Cela vaut la peine d'ajouter quelques lignes supplémentaires car la ligne d'en-tête reste synchronisée avec les clés du dictionnaire ; personne n'a besoin de se souvenir de la colonne qui a été indexée 3. La même records liste alimente les trois rédacteurs, de sorte que changer de format ne nécessite qu’une ligne de code en production.

Solution de secours : affichez le tableau avec Playwright lorsque le réseau est indisponible

Certains sites ne vous laissent véritablement pas accéder au JSON. Les URL signées qui expirent en quelques secondes, les points de terminaison GraphQL avec des Origin , les grilles alimentées par WebSocket et une poignée de configurations sur mesure vous poussent toutes à afficher la page dans un vrai navigateur. Playwright pour Python est un excellent choix moderne par défaut pour cette tâche, bien que Selenium reste un choix raisonnable sur les piles héritées.

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example.com/grid", wait_until="networkidle")
    page.wait_for_selector("table.grid tbody tr")
    rows = page.locator("table.grid tbody tr").all_text_contents()
    browser.close()

Un piège à surveiller dans toute solution de secours pour l'extraction de données de tableaux JavaScript : les bibliothèques de grilles côté client telles que DataTables, AG Grid et TanStack Table virtualisent généralement le rendu, ce qui signifie que seules les lignes actuellement visibles dans la fenêtre d'affichage sont chargées dans le DOM à un moment donné. Le nombre exact de lignes dépend de la taille de la fenêtre d'affichage et de la configuration de la bibliothèque ; ne vous fiez donc pas à une simple tr collection naïve pour tout capturer. Faites défiler le conteneur en boucle, surveillez l'arrivée de nouvelles lignes avec un MutationObserver, ou appelez l'API de pagination propre à la bibliothèque jusqu'à ce que le nombre total de lignes cesse d'augmenter.

Pièges courants lors du web scraping de tableaux JavaScript

La plupart des échecs lors du web scraping de tableaux JavaScript sont silencieux. Le script s'exécute, le fichier est écrit, et personne ne remarque que les données sont erronées jusqu'à ce qu'un tableau de bord le signale. Soyez attentif aux points suivants :

  • Sélectionner les tableaux par index. tables[2] ne fonctionne plus dès que le service marketing ajoute un widget de comparaison au-dessus de la grille. Effectuez plutôt la correspondance par le texte de la légende, l'ID ou un en-tête unique.
  • Grilles virtualisées. Un scraping naïf sur DataTables, AG Grid ou TanStack Table ne peut capturer que les lignes visibles dans la fenêtre d'affichage, tandis que des milliers d'autres restent non chargées. Vérifiez le nombre total de lignes par rapport au compte API ou à une requête paginée.
  • Chiffres formatés selon les paramètres régionaux. 1.000,50 est européen pour 1000.50, mais Python float() le lit comme 1.0. Normalisez la chaîne avant la conversion.
  • Fuseaux horaires dans les dates. "2025-04-01" analysées sans fuseau horaire deviennent silencieusement minuit UTC, ce qui décale les agrégats quotidiens d'une ligne.
  • Symboles monétaires et séparateurs de milliers. "$1,234" ne sera pas converti en nombre à virgule flottante. Supprimez d'abord les caractères non numériques.
  • Cookies expirés. Un cookie de session collé fonctionne pendant une journée, puis renvoie discrètement des codes 401 que certains serveurs déguisent en HTML HTTP 200.
  • Réponses 200 anti-bot. Un WAF peut renvoyer une page de captcha avec un statut 200. r.json() lève une exception, mais uniquement si vous pensez à l'appeler.

Valider et surveiller le pipeline d'extraction

Un scraping n'est pas terminé dès la « création du CSV ». Il est terminé lorsque vous pouvez vous fier au fichier le lendemain. Ajoutez une petite couche de validation après l'enregistreur : vérifiez que le nombre de lignes se situe dans une fourchette raisonnable par rapport à l'exécution d'hier, signalez clairement un échec si une colonne requise présente un taux de valeurs nulles supérieur à un seuil (1 à 5 % convient), et comparez l'ensemble des colonnes à un manifeste enregistré afin qu'un champ renommé signale un décalage de schéma au lieu de corrompre une jointure en aval. Signalez séparément les exécutions sans ligne. La plupart des pipelines de scraping de tableaux JavaScript meurent d'un rétrécissement silencieux, et non de plantages bruyants.

Points clés

  • La voie par défaut pour le web scraping de tableaux JavaScript est le point de terminaison JSON caché, et non un navigateur sans interface graphique. Utilisez l'échelle de décision avant d'écrire le moindre code.
  • L'onglet Réseau de DevTools, associé à une action de tri ou de pagination déclenchée, est le moyen le plus rapide de mettre en évidence l'appel qui transporte réellement les lignes.
  • Reproduisez la requête sans état : en-têtes publics, raise_for_status(), une session réelle pour les connexions, et jamais un cookie personnel copié-collé manuellement.
  • Les modèles de pagination varient (DataTables draw/start/length, curseurs, décalages) ; considérez la boucle, et non la requête unique, comme l'unité de travail.
  • Playwright est l'outil idéal lorsque le chemin réseau est signé, chiffré ou absent, et uniquement dans ce cas. Méfiez-vous des grilles virtualisées qui ne montent que les lignes de la fenêtre d'affichage.
  • Un pipeline que vous pouvez réexécuter au trimestre prochain comporte des assertions sur le nombre de lignes, des seuils de taux de nullité et un manifeste de colonnes, et pas seulement un fichier CSV fonctionnel aujourd’hui.

FAQ

Pourquoi requests.get() renvoie-t-il des lignes vides pour un tableau JavaScript ?

Parce que requests n'exécute pas JavaScript. Il télécharge le document initialement servi par le serveur, qui contient la structure de la page et un ensemble de scripts, mais aucune ligne. Les lignes sont ajoutées ultérieurement par du code côté client appelant un point de terminaison JSON. Votre analyseur voit la structure vide <table> et ne renvoie rien.

Ai-je vraiment besoin de Selenium ou de Playwright pour extraire une table dynamique ?

En général, non. Si DevTools affiche une requête JSON qui alimente la grille, rejouer cette requête avec requests ou httpx est plus rapide, moins coûteux et plus fiable qu'un navigateur. N'utilisez Playwright que lorsque l'appel est signé, en GraphQL avec des vérifications d'origine strictes, piloté par WebSocket, ou autrement inaccessible depuis un simple client HTTP.

Comment extraire les données d'un tableau JavaScript qui nécessite une connexion ou un jeton CSRF ?

Utilisez un requests.Session afin que les cookies persistent d'un appel à l'autre. Envoyez vos identifiants à l'endpoint de connexion, puis lisez la valeur CSRF à partir d'un champ masqué ou du XSRF-TOKEN cookie et transmettez-la en tant qu'en-tête dans la requête de données. Ne codez jamais en dur un cookie de session copié depuis votre propre navigateur.

Que faire si l'API cachée ne renvoie qu'une seule page de lignes à la fois ?

Faites une boucle. Inspectez les paramètres de la requête (start, length, cursor, page, offset) et incrémentez-les jusqu’à ce que la réponse renvoie zéro ligne ou un has_more: false indicateur. Ajoutez un délai d'attente exponentiel en cas de code d'erreur HTTP 429 et une limite stricte de requêtes afin qu'un bug côté serveur ne puisse pas plonger votre scraper dans une boucle infinie.

Conclusion

Le scraping de tableaux JavaScript cesse d’être effrayant dès lors que vous cessez de considérer la page affichée comme la source de vérité. Le navigateur est un moteur de rendu ; le point de terminaison JSON derrière la grille est la véritable source de données. Trouvez ce point de terminaison dans DevTools, relancez-le avec requests, paginez-le correctement, validez la sortie, et vous obtiendrez un script qui survivra à la prochaine refonte au lieu d’un script qui remplit discrètement votre entrepôt de lignes vides.

Réservez le navigateur headless aux cas qui en ont véritablement besoin. Les sites avec des appels réseau signés, des grilles alimentées par WebSocket ou une protection anti-bot agressive vous y pousseront, et c’est exactement là qu’un plan de secours prend toute son importance. Lorsque vous avez recours à un navigateur, soyez vigilant quant au rendu virtualisé, validez les totaux des lignes et maintenez votre couche de surveillance en place.

Si vous préférez ne pas gérer vous-même la rotation des proxys, les empreintes de navigateur et les gestionnaires de CAPTCHA, WebScrapingAPI peut se placer devant votre requests et renvoyer du HTML ou du JSON propre à partir de sites qui bloqueraient autrement l'accès direct, sans modifier la logique d'analyse et de pagination en amont. Quelle que soit la voie que vous choisissez, la stratégie est la même : optez pour le chemin d'extraction le moins coûteux qui fonctionne, et concevez le script de manière suffisamment honnête pour qu'il vous signale quand il cesse de fonctionner.

À 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.