Le défi du contenu rendu par JavaScript
Les sites Web modernes comptent de plus en plus sur JavaScript pour rendre le contenu. Les applications à une page (SPA) construites avec React, Vue ou Angular chargent un shell HTML minimal, puis récupèrent et rendent les données côté client. Lorsque vous faites une simple requête HTTP à ces sites, vous obtenez une page vide ou incomplète parce que le contenu n'existe qu'après l'exécution de JavaScript.
Scraping JavaScript-heavy sites Web nécessite navigateurs sans tête — de véritables moteurs de navigateur fonctionnant sans fenêtre visible pouvant exécuter JavaScript, rendre DOM et interagir avec des éléments de page. Combiné avec des proxies, navigateurs sans tête déverrouillent les données des sites Web les plus dynamiques.
Ce guide fait partie de notre Guide complet des produits de scraping Web. Pour éviter la détection en utilisant des navigateurs sans tête, voir Comment les systèmes anti-bot détectent les proxies.
Quand avez-vous besoin d'un navigateur sans tête?
| Scénario | Simple HTTP | Navigateur sans tête |
|---|---|---|
| Pages HTML statiques | Fonctionne parfaitement | Surcapacité |
| Pages rendues par le serveur avec API | Fonctionne (toucher l'API directement) | Pas nécessaire |
| SPA (Réaction, Vue, Angulaire) | Obtient un shell vide | Requis |
| défilement infini / chargement paresseux | Impossible de déclencher | Requis |
| Contenu derrière connexion + JS | Difficulté | Recommandation |
| Pages avec contrôles JS antibot | Détection des défaillances | Requis |
Vérifiez toujours si le site a une API ou un rendu côté serveur avant d'atteindre un navigateur sans tête. Beaucoup de sites "JavaScript-heavy" ont en fait des paramètres d'API qui retournent JSON propre - beaucoup plus rapide et moins cher pour racler.
Puppeteer + Proxies (Node.js)
Puppeteer contrôle Chrome / Chrome programmatiquement. C'est l'outil de navigateur sans tête le plus mature pour Node.js.
Configuration de base avec ProxyHat
const puppeteer = require('puppeteer');
async function scrapeWithPuppeteer(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
});
const page = await browser.newPage();
// Authenticate with proxy
await page.authenticate({
username: 'USERNAME',
password: 'PASSWORD',
});
// Set realistic viewport and user agent
await page.setViewport({ width: 1920, height: 1080 });
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
// Wait for specific content to render
await page.waitForSelector('.product-list', { timeout: 10000 });
const content = await page.content();
const data = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.product-item')).map(el => ({
name: el.querySelector('.product-name')?.textContent?.trim(),
price: el.querySelector('.product-price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}));
});
return { html: content, data };
} finally {
await browser.close();
}
}
// Usage
const result = await scrapeWithPuppeteer('https://example.com/products');
console.log(`Found ${result.data.length} products`);Tirage optimisé multi-pages
const puppeteer = require('puppeteer');
class PuppeteerScraper {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.browser = null;
}
async init() {
this.browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
],
});
}
async scrapePage(url) {
const page = await this.browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
await page.setViewport({ width: 1920, height: 1080 });
// Block unnecessary resources to speed up loading
await page.setRequestInterception(true);
page.on('request', (req) => {
const type = req.resourceType();
if (['image', 'stylesheet', 'font', 'media'].includes(type)) {
req.abort();
} else {
req.continue();
}
});
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const content = await page.content();
return { url, status: 'success', html: content };
} catch (err) {
return { url, status: 'error', error: err.message };
} finally {
await page.close();
}
}
async scrapeMany(urls) {
const results = [];
for (let i = 0; i < urls.length; i += this.concurrency) {
const batch = urls.slice(i, i + this.concurrency);
const batchResults = await Promise.all(
batch.map(url => this.scrapePage(url))
);
results.push(...batchResults);
console.log(`Progress: ${results.length}/${urls.length}`);
}
return results;
}
async close() {
if (this.browser) await this.browser.close();
}
}
// Usage
const scraper = new PuppeteerScraper(3);
await scraper.init();
const results = await scraper.scrapeMany(urls);
await scraper.close();Dramaturge + Proxies (Python)
Playwright est une alternative plus récente qui prend en charge Chrome, Firefox et WebKit. Son API Python est propre et bien adapté pour le grattage.
Configuration de base
from playwright.sync_api import sync_playwright
def scrape_with_playwright(url: str) -> dict:
"""Scrape a JavaScript-heavy page using Playwright with ProxyHat proxy."""
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
)
page = context.new_page()
try:
page.goto(url, wait_until="networkidle", timeout=60000)
# Wait for dynamic content
page.wait_for_selector(".product-list", timeout=10000)
# Extract data using page.evaluate
products = page.evaluate("""() => {
return Array.from(document.querySelectorAll('.product-item')).map(el => ({
name: el.querySelector('.product-name')?.textContent?.trim(),
price: el.querySelector('.product-price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}));
}""")
return {"url": url, "products": products, "html": page.content()}
finally:
browser.close()Async Playwright pour scraping parallèle
import asyncio
from playwright.async_api import async_playwright
async def scrape_batch(urls: list[str], concurrency: int = 3) -> list[dict]:
"""Scrape multiple JS-heavy pages in parallel using Playwright."""
results = []
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
semaphore = asyncio.Semaphore(concurrency)
async def scrape_one(url: str) -> dict:
async with semaphore:
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
)
page = await context.new_page()
# Block heavy resources
await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2}",
lambda route: route.abort())
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
html = await page.content()
return {"url": url, "status": "success", "html": html}
except Exception as e:
return {"url": url, "status": "error", "error": str(e)}
finally:
await context.close()
tasks = [scrape_one(url) for url in urls]
results = await asyncio.gather(*tasks)
await browser.close()
return results
# Usage
urls = [f"https://example.com/product/{i}" for i in range(50)]
results = asyncio.run(scrape_batch(urls, concurrency=5))Go: Utilisation de chromedp avec Proxies
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/chromedp"
)
func scrapeJSPage(targetURL string) (string, error) {
// Configure proxy
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("http://gate.proxyhat.com:8080"),
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
var htmlContent string
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.WaitVisible(".product-list", chromedp.ByQuery),
chromedp.OuterHTML("html", &htmlContent),
)
if err != nil {
return "", fmt.Errorf("scrape failed: %w", err)
}
return htmlContent, nil
}
func main() {
html, err := scrapeJSPage("https://example.com/products")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got %d bytes of rendered HTML\n", len(html))
}Stratégies d'optimisation des performances
Les navigateurs sans tête sont 10-50x plus lents que les simples requêtes HTTP. Voici des stratégies pour minimiser l'écart de rendement :
1. Bloc des ressources inutiles
Les images, CSS, polices et fichiers multimédias ne sont pas nécessaires pour l'extraction des données. Les bloquer accélère considérablement les charges de pages :
# Playwright resource blocking
async def fast_scrape(page, url):
# Block images, CSS, fonts, media
await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2,mp4,webm}",
lambda route: route.abort())
# Also block tracking scripts
await page.route("**/*google-analytics*", lambda route: route.abort())
await page.route("**/*facebook*", lambda route: route.abort())
await page.goto(url, wait_until="domcontentloaded") # Faster than networkidle
return await page.content()2. Utiliser la bonne stratégie d'attente
| Stratégie | Vitesse | Fiabilité | Cas d'utilisation |
|---|---|---|---|
domcontentloaded | Rapide | Peut manquer les données d'async | Pages avec données en ligne |
load | Moyenne | Bonne | La plupart des pages |
networkidle | Lentement | Plus haut | Spas lourds, défilement infini |
| Sélecteur spécifique | Variable | Plus haut | Quand vous connaissez l'élément cible |
3. Réutiliser les instances du navigateur
Lancer un navigateur prend 1-3 secondes. Pour le grattage par lots, lancez une fois et créez de nouvelles pages/contextes pour chaque URL :
from playwright.sync_api import sync_playwright
class BrowserPool:
"""Reusable browser pool for efficient headless scraping."""
def __init__(self, pool_size: int = 3):
self.pool_size = pool_size
self.playwright = None
self.browsers = []
def start(self):
self.playwright = sync_playwright().start()
for _ in range(self.pool_size):
browser = self.playwright.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "USERNAME",
"password": "PASSWORD",
}
)
self.browsers.append(browser)
def get_browser(self, index: int):
return self.browsers[index % self.pool_size]
def stop(self):
for browser in self.browsers:
browser.close()
self.playwright.stop()
# Usage
pool = BrowserPool(pool_size=3)
pool.start()
for i, url in enumerate(urls):
browser = pool.get_browser(i)
context = browser.new_context()
page = context.new_page()
page.goto(url, wait_until="networkidle")
html = page.content()
context.close()
pool.stop()4. Intercepter les appels API au lieu d'analyser DOM
De nombreux SPA récupèrent les données des API. Interceptez ces appels d'API directement — vous obtenez JSON propre sans analyse HTML:
const puppeteer = require('puppeteer');
async function interceptAPIData(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--proxy-server=http://gate.proxyhat.com:8080'],
});
const page = await browser.newPage();
await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
const apiResponses = [];
// Intercept XHR/fetch responses
page.on('response', async (response) => {
const url = response.url();
if (url.includes('/api/') || url.includes('/graphql')) {
try {
const json = await response.json();
apiResponses.push({ url, data: json });
} catch {
// Not JSON, skip
}
}
});
await page.goto(url, { waitUntil: 'networkidle2' });
await browser.close();
return apiResponses;
}
// Get clean API data instead of scraping DOM
const data = await interceptAPIData('https://example.com/products');
console.log(`Intercepted ${data.length} API calls`);Comparaison entre navigateur sans tête et HTTP
| métrique | Simple HTTP + Proxy | Navigateur sans tête + Proxy |
|---|---|---|
| Vitesse par page | 0,5-2 secondes | 3-15 secondes |
| Mémoire par instance | ~50 Mo | 200-500 Mo |
| Utilisation du processeur | Minimale | Important |
| Bande passante par page | 50-200 KB | 2-10 MB (avec ressources) |
| rendu JavaScript | Numéro | Total |
| Voie de contournement antibots | Limitée | Mieux (navigateur réel) |
| Pages simultanées | 100 ans et plus | 3-10 par machine |
Meilleures pratiques
- Essayez toujours HTTP en premier. Vérifiez les paramètres d'API, le contenu rendu par le serveur ou JSON intégré dans le HTML avant d'utiliser un navigateur sans tête.
- Bloquer les ressources inutiles. Images, CSS et polices ajoutent du temps de chargement sans fournir de données.
- Utilisez des sélecteurs spécifiques pour attendre.
networkidleest sûr mais lent. Attendez l'élément spécifique dont vous avez besoin. - Réutiliser les instances du navigateur. Lancez une fois, créez de nouveaux contextes par page.
- Interceptez les appels d'API. De nombreux SPA chargent les données via les API — interceptez directement le JSON.
- Limitez la concordance. Les navigateurs sans tête sont à forte intensité de mémoire. 3-5 pages simultanées par Go de RAM est une bonne règle.
- Utilisez des procurations résidentielles. ProxyHat proxies résidentielles fournir les scores de confiance les plus élevés, réduisant la détection lors de l'exécution de navigateurs sans tête.
Pour manipuler les CAPTCHA que les navigateurs sans tête rencontrent, voir Traitement des CAPTCHA Lors du scraping. Pour le grattage du navigateur sans tête, lire Comment évaluer l'infrastructure de scraping.
Commencez par Python SDK, Numéro SDKou Allez au SDK pour l'intégration par procuration, et explorer ProxyHat pour le scraping Web.
Foire aux questions
Ai-je toujours besoin d'un navigateur sans tête pour les sites JavaScript ?
C'est pas vrai. De nombreux sites JavaScript-lourds chargent les données à partir des paramètres de l'API. Vérifiez l'onglet Réseau du navigateur pour les requêtes XHR/fetch — si les données proviennent d'une API, vous pouvez appeler cette API directement avec des requêtes HTTP simples via un proxy, ce qui est beaucoup plus rapide.
Puppeteer ou Playwright — qui est mieux pour le grattage?
Le dramaturge est généralement recommandé pour de nouveaux projets. Il prend en charge plusieurs moteurs de navigateur (Chromium, Firefox, WebKit), a mieux auto-attente, native async support en Python, et la configuration proxy intégrée. Puppeteer est plus mature et a un écosystème plus grand si vous êtes dans le monde Node.js.
Combien de pages de navigateur sans tête puis-je exécuter simultanément?
Chaque page consomme 200-500 Mo de RAM. Sur une machine avec 8 Go de RAM, 3-10 pages simultanées est réaliste. Utilisez le blocage des ressources (images, CSS) pour réduire la mémoire. Pour une plus grande cohérence, distribuez sur plusieurs machines en utilisant une architecture basée sur la file d'attente.
Pourquoi utiliser des proxies avec des navigateurs sans tête ?
Même avec un vrai navigateur, les requêtes répétées de la même IP sont bloquées. Proxies tourne votre IP de sorte que chaque charge de page semble venir d'un utilisateur différent. Les procurations résidentielles par l'intermédiaire de ProxyHat fournissent les scores de confiance les plus élevés, minimisant les blocs et les CAPTCHA.






