Vous écrivez quelques lignes de Python, pointez requests vers une page de liste de produits ou une page de résultats de recherche, passez la réponse à BeautifulSoup, et n'obtenez presque rien en retour. Le titre est là, la mise en page est là, mais les données que vous souhaitiez réellement sont manquantes. C'est le mur le plus courant que les gens rencontrent lorsqu'ils tentent de scraper des pages JavaScript avec Python : la page rend son contenu dans le navigateur, après l'arrivée du HTML initial, donc une simple requête HTTP ne voit jamais que la coquille vide.
Ce guide explique pourquoi cela se produit, présente les trois vraies manières d'obtenir les données rendues (navigateurs headless, l'API JSON sous-jacente, et une API de rendu), et montre un exemple propre et exécutable qui récupère une page complète via la Crawling API et l'analyse avec BeautifulSoup. À la fin, vous saurez quelle approche convient à quel travail et comment éviter qu'un run ne soit bloqué.
Pourquoi requests et BeautifulSoup renvoient une coquille vide
Pour voir le problème plutôt que de le lire, récupérez une page rendue côté client de manière naïve et regardez ce qui revient.
import requests from bs4 import BeautifulSoup url = "https://example-shop.com/search?q=smartwatch" html = requests.get(url).text soup = BeautifulSoup(html, "html.parser") products = soup.select("[data-product-title]") print(f"found {len(products)} products") # found 0 products
Code de statut 200, un document HTML d'apparence complète, et zéro produit. La raison tient au cycle de vie de la page. Le serveur envoie un squelette HTML léger : quelques points de montage div, des balises <script>, peut-être un spinner de chargement. Ce n'est qu'une fois ces scripts exécutés que le navigateur appelle une API, reçoit les données produit en JSON et construit les nœuds DOM qui les contiennent. La bibliothèque requests n'exécute pas JavaScript. Elle télécharge le squelette et s'arrête, donc les nœuds produit n'existent jamais pour que BeautifulSoup les trouve.
La correction pour chaque approche ci-dessous est la même en principe : amener la page à un état où le JavaScript a déjà été exécuté, puis analyser cet état. Les approches ne diffèrent que par la manière dont elles atteignent cet état rendu et ce que cela vous coûte en vitesse, en infrastructure et en risque d'être bloqué.
Cliquez droit sur la page et choisissez "Afficher la source" pour voir le HTML brut envoyé par le serveur, qui est exactement ce que requests obtient. Puis ouvrez les outils de développement et regardez le panneau Éléments, qui montre le DOM en direct après l'exécution des scripts. Si vos données cibles apparaissent dans Éléments mais pas dans Afficher la source, la page est rendue côté client et une simple requête ne fonctionnera pas.
Approche 1 : piloter un vrai navigateur avec Selenium ou Playwright
La correction la plus directe consiste à utiliser un outil qui exécute réellement un navigateur. Selenium et Playwright lancent tous deux Chromium (headless ou visible), chargent l'URL, attendent la fin des scripts et vous permettent de lire le DOM rendu. Comme un véritable moteur de navigateur exécute le JavaScript, les données manquantes d'une simple requête sont maintenant présentes.
Un exemple minimal avec Playwright ressemble à ceci :
from playwright.sync_api import sync_playwright from bs4 import BeautifulSoup with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example-shop.com/search?q=smartwatch") page.wait_for_selector("[data-product-title]") html = page.content() browser.close() soup = BeautifulSoup(html, "html.parser") titles = [t.get_text(strip=True) for t in soup.select("[data-product-title]")] print(titles)
La ligne clé est wait_for_selector. Plutôt que de deviner avec un sleep fixe, vous dites au navigateur d'attendre jusqu'à ce que l'élément qui vous intéresse existe réellement, ce qui est à la fois plus rapide et plus fiable. Selenium offre la même idée via ses helpers WebDriverWait et expected-conditions.
Cette approche fonctionne, et c'est le bon outil quand vous devez cliquer, faire défiler, remplir des formulaires ou parcourir des flux de plusieurs pages. Mais elle a de vrais coûts. Chaque instance de navigateur consomme des centaines de mégaoctets de RAM et un cœur CPU complet, donc en exécuter plusieurs en parallèle est coûteux. La configuration est délicate : vous gérez des binaires de navigateur, des versions de pilotes et une chaîne de dépendances fragile. Et le rendu seul ne vous rend pas invisible. Un navigateur headless depuis une adresse IP de datacenter, avec une empreinte d'automatisation par défaut, est détecté et bloqué par les systèmes anti-bot sérieux aussi vite qu'une simple requête. Le rendu résout le problème JavaScript ; il ne fait rien pour le problème de détection. Pour une comparaison plus complète des moteurs, consultez choisir un navigateur headless pour le web scraping et ce tutoriel sur scraper du contenu dynamique avec Selenium et BeautifulSoup.
Approche 2 : ignorer le navigateur et appeler l'API sous-jacente
Voici ce que la plupart des tutoriels manquent. Quand une page rendue côté client se construit, elle récupère presque toujours ses données depuis un point de terminaison JSON backend. Si vous pouvez trouver ce point de terminaison, vous pouvez l'appeler directement et ignorer entièrement le rendu, obtenant du JSON structuré propre sans aucun navigateur.
Pour le trouver, ouvrez les outils de développement, allez dans l'onglet Réseau, filtrez sur Fetch/XHR et rechargez la page. Vous cherchez une requête dont la réponse contient vos données, généralement une URL avec /api/, /graphql ou un chemin chargé de paramètres. Une fois repérée, répliquez-la en Python.
import requests api = "https://example-shop.com/api/search" params = {"q": "smartwatch", "page": 1} headers = {"Accept": "application/json"} data = requests.get(api, params=params, headers=headers).json() for item in data["results"]: print(item["title"], item["price"])
Quand cela fonctionne, c'est de loin l'option la plus efficace : pas de surcharge de navigateur, des données structurées plutôt que du HTML à analyser, et une pagination intégrée via les propres paramètres de l'API. Ça vaut toujours dix minutes dans l'onglet Réseau avant de recourir à quelque chose de plus lourd.
Le problème est que cela ne fonctionne pas toujours. Le point de terminaison peut nécessiter un token signé, un cookie de session ou un ensemble d'en-têtes spécifiques que la page génère dynamiquement. Il peut être protégé par la même couche anti-bot que la page elle-même. Et il peut changer sans préavis, puisqu'une API interne n'a aucune garantie de stabilité. Quand l'API est accessible, utilisez-la. Quand elle est verrouillée, vous avez besoin d'une page rendue, ce qui nous amène à la troisième approche.
Approche 3 : rendre via la Crawling API et analyser le résultat
Les deux approches précédentes résolvent chacune la moitié du problème. Un navigateur headless rend mais ne vous cache pas. Un appel direct à l'API est propre mais souvent bloqué. Ce que vous voulez généralement, c'est les deux à la fois : un vrai navigateur qui exécute le JavaScript de la page, derrière une adresse IP que le site perçoit comme un visiteur authentique, retournant le HTML complet en un seul appel pour que votre Python reste simple.
C'est ce que fait la Crawling API. Vous lui envoyez une URL avec un token JavaScript, elle charge la page dans un vrai navigateur de son côté, fait tourner des adresses IP résidentielles côté serveur et vous retourne le HTML entièrement rendu. Vous ne gérez jamais une flotte de navigateurs ni ne maintenez un pool de proxies ; vous faites une seule requête HTTP et analysez la réponse avec le même BeautifulSoup que vous connaissez déjà.
Crawlbase propose deux types de tokens. Le token normal récupère le HTML statique ; le token JavaScript (JS) rend d'abord la page dans un vrai navigateur. Pour une cible rendue côté client, vous avez besoin du token JS, sinon vous récupérez la même coquille vide qu'une simple requête et il n'y a rien à analyser.
Installez le client officiel et BeautifulSoup, puis récupérez la page rendue.
python -m venv scraper_env source scraper_env/bin/activate pip install crawlbase beautifulsoup4
Sur Windows, activez l'environnement avec scraper_env\Scripts\activate au lieu de la ligne source. Maintenant récupérez la page avec le token JS et les deux options d'attente qui importent pour le contenu rendu côté client.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") print(f"Request failed: {response['status_code']}") return None if __name__ == "__main__": page_url = "https://example-shop.com/search?q=smartwatch" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
Les deux options d'attente font le travail pour une cible rendue côté client. ajax_wait indique à l'API d'attendre que les requêtes asynchrones se stabilisent avant de capturer la page, et page_wait maintient un délai fixe en millisecondes après le chargement pour que les éléments à rendu tardif apparaissent. Cinq secondes est un bon point de départ ; augmentez si vos champs reviennent vides. Exécutez ceci et vous devriez voir le vrai balisage dans les 500 premiers caractères, pas le squelette qu'une simple requête renvoie. Cela confirme que le rendu fonctionne avant d'écrire le moindre sélecteur.
Rendre une page JavaScript derrière une adresse IP de confiance, en un seul appel, est exactement ce pour quoi la Crawling API est faite. Passez un token JS, elle exécute la page dans un vrai navigateur, fait tourner des adresses IP résidentielles côté serveur et retourne le HTML complet, vous évitant ainsi de gérer vous-même une flotte headless et un pool de proxies. Testez sur une vraie page sur le niveau gratuit d'abord.
Analyser le HTML rendu avec BeautifulSoup
Une fois que crawl retourne le HTML rendu, l'étape d'analyse est du BeautifulSoup ordinaire, car le JavaScript a déjà été exécuté côté serveur et les nœuds de données sont présents. Encapsulez l'accès aux champs dans un petit helper pour qu'un élément manquant ne fasse pas planter l'exécution.
import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["status_code"] == 200: return response["body"].decode("utf-8") return None def parse_products(html): soup = BeautifulSoup(html, "html.parser") items = [] for card in soup.select("div.product-card"): title = card.select_one("[data-product-title]") price = card.select_one("span.price") items.append({ "title": title.get_text(strip=True) if title else None, "price": price.get_text(strip=True) if price else None, }) return items def main(): url = "https://example-shop.com/search?q=smartwatch" html = crawl(url) if not html: return products = parse_products(html) print(json.dumps(products, indent=2)) if __name__ == "__main__": main()
Exécutez-le avec python scraper.py et vous obtenez une liste structurée propre, prête à écrire en JSON, CSV ou dans une base de données.
[ { "title": "Aero Fit Smartwatch 2", "price": "$129.00" }, { "title": "Pulse Sport Band Pro", "price": "$89.99" } ]
Les noms de classe et les attributs de données changent à mesure que les sites se redesignent, donc un sélecteur qui fonctionnait le mois dernier peut ne rien retourner aujourd'hui. Quand un champ revient comme None, inspectez à nouveau la page en direct dans les outils de développement et mettez à jour le sélecteur. Une maintenance périodique des sélecteurs est normale pour tout scraper en production, ce n'est pas le signe que quelque chose est cassé.
Pièges courants lors du scraping de pages JavaScript
Quelques problèmes représentent la plupart des runs échoués contre les cibles rendues côté client. Les connaître à l'avance évite beaucoup de débogage.
-
Capturer trop tôt. L'erreur la plus fréquente est d'analyser avant que le contenu n'existe. Préférez attendre un sélecteur spécifique ou, avec la Crawling API, misez sur
ajax_waitet unpage_waitgénéreux plutôt qu'un délai fixe aveugle. - Contenu derrière une interaction. Certaines données n'apparaissent qu'après un défilement, un clic sur un onglet ou un appui sur "charger plus". Une simple requête ou un rendu unique ne déclenchera pas cela. C'est là qu'un navigateur que vous scriptez étape par étape, ou un rendu avec une instruction de défilement, justifie son coût.
- Listes en chargement différé et paginées. Les pages à défilement infini se chargent par morceaux au fur et à mesure que vous faites défiler. Soit pilotez le défilement dans un navigateur, soit, mieux, trouvez l'API paginée derrière et demandez chaque page directement.
- Être bloqué malgré le rendu. Le rendu n'est pas de la discrétion. Une adresse IP de datacenter ou une empreinte d'automatisation évidente se fait quand même challenger. La rotation des adresses IP résidentielles est ce qui maintient réellement un run en vie à grande échelle.
Choisir une approche
Il n'y a pas un seul bon outil, seulement le bon outil pour le travail en face de vous.
Cherchez d'abord un appel API direct. Si l'onglet Réseau révèle un point de terminaison JSON ouvert, c'est le chemin le plus propre et le plus rapide, sans surcharge de rendu. Vérifiez toujours avant de faire quoi que ce soit de plus lourd.
Utilisez un navigateur scripté quand vous avez besoin d'interaction. Les connexions, les formulaires en plusieurs étapes, les clics et le contenu déclenché par le défilement nécessitent tous Selenium ou Playwright, où vous contrôlez la session étape par étape. Acceptez le coût en mémoire et en configuration comme le prix de ce contrôle.
Utilisez une API de rendu quand vous avez besoin de HTML complet à grande échelle sans être bloqué. Quand le travail est "récupérer de nombreuses pages JavaScript de manière fiable et les analyser", la Crawling API supprime les deux parties les plus difficiles, l'exécution des navigateurs et la rotation des adresses IP, et ne vous laisse qu'un appel HTTP en plus de BeautifulSoup. Si vous préférez router votre propre trafic de navigateur via un pool rotatif, le Smart AI Proxy (aussi appelé AI Proxy) vous donne la rotation résidentielle comme point de terminaison proxy. Pour un tour plus large de ces patterns, consultez comment crawler des sites JavaScript.
Points clés
-
Les simples requêtes ne voient que le squelette.
requestsn'exécute pas JavaScript, donc les données rendues côté client sont absentes du HTML qu'il télécharge. - Trois vraies corrections existent. Piloter un vrai navigateur, appeler l'API JSON sous-jacente directement, ou rendre via une API qui retourne le HTML complet.
- Vérifiez d'abord une API ouverte. Un point de terminaison JSON direct est le chemin le plus rapide et le plus propre quand il est accessible, sans coût de rendu.
- Le rendu n'est pas de la discrétion. Un navigateur headless sur une adresse IP de datacenter se fait quand même bloquer ; la rotation des adresses IP résidentielles est ce qui maintient un run en vie.
-
La Crawling API combine les deux. Un token JS rend la page derrière une adresse IP de confiance en un seul appel ;
ajax_waitetpage_waitcontrôlent la durée d'attente avant que BeautifulSoup n'analyse le résultat.
Foire aux questions
Pourquoi requests ne renvoie-t-il pas de données sur une page JavaScript ?
Parce que requests ne télécharge que le HTML envoyé par le serveur et n'exécute jamais JavaScript. Une page rendue côté client envoie un squelette fin et construit ensuite son vrai contenu dans le navigateur en appelant une API après le chargement. Comme cette étape ne se produit jamais dans une simple requête, les nœuds de données n'existent pas quand BeautifulSoup analyse la réponse, donc vos sélecteurs ne correspondent à rien.
Ai-je toujours besoin d'un navigateur headless pour scraper des pages JavaScript avec Python ?
Non. Un navigateur headless est une option, mais souvent la plus lourde. Avant de lancer Selenium ou Playwright, ouvrez l'onglet Réseau et cherchez le point de terminaison JSON que la page appelle. S'il est accessible, l'appeler directement avec requests est plus rapide et plus propre. Recourez à un navigateur, ou à une API de rendu, uniquement quand aucun point de terminaison ouvert n'est disponible ou que les données nécessitent une interaction.
Quelle est la différence entre ajax_wait et page_wait ?
ajax_wait indique à la Crawling API d'attendre que les requêtes asynchrones (XHR/fetch) de la page se stabilisent avant de capturer le HTML, ce qui remplit les données rendues côté client. page_wait ajoute un délai fixe en millisecondes après le chargement, donnant aux éléments à rendu tardif plus de temps pour apparaître. Utilisez les deux pour les cibles rendues côté client, et augmentez page_wait si des champs reviennent vides.
Pourquoi mon navigateur headless se fait-il quand même bloquer ?
Parce que le rendu et la discrétion sont des problèmes séparés. Exécuter un vrai navigateur résout le problème d'exécution JavaScript, mais la requête provient toujours d'une adresse IP reconnaissable et d'une empreinte d'automatisation. Les systèmes anti-bot signalent les adresses IP de datacenter et les signatures headless par défaut indépendamment du rendu. La rotation des adresses IP résidentielles, que fournissent la Crawling API et Smart AI Proxy, est ce qui traite le problème de blocage.
Puis-je utiliser BeautifulSoup avec la Crawling API ?
Oui, et c'est le workflow prévu. La Crawling API retourne du HTML entièrement rendu, vous l'analysez donc avec BeautifulSoup exactement comme n'importe quelle page statique. La différence est que le JavaScript a déjà été exécuté côté serveur, donc les nœuds de données que vos sélecteurs ciblent sont présents dans le HTML que vous recevez.
Comment scraper des pages JavaScript qui chargent plus de contenu au défilement ?
Les pages à défilement infini se chargent par morceaux au fur et à mesure que l'utilisateur fait défiler, donc une seule requête ou un seul rendu ne capture que le premier lot. Vous avez deux options : scripter le défilement dans Selenium ou Playwright et attendre chaque lot, ou trouver l'API paginée que le défilement déclenche dans l'onglet Réseau et demander chaque page directement. La voie API directe est généralement plus rapide et plus fiable quand le point de terminaison est accessible.
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.
