Cómo scrapear sitios web con mucho JavaScript

Arrastre el contenido de JavaScript con navegadores y proxies sin cabeza. Guías de configuración Puppeteer, Playwright y chromedp con optimización de rendimiento y estrategias de intercepción API.

Cómo scrapear sitios web con mucho JavaScript

El reto del contenido remitido por JavaScript

Los sitios web modernos dependen cada vez más de JavaScript para renderizar contenido. Aplicaciones de una sola página (SPAs) construidas con React, Vue o Angular cargan un mínimo de shell HTML, luego capturar y renderizar datos lado cliente. Cuando usted hace una simple solicitud HTTP a estos sitios, usted obtiene una página vacía o incompleta porque el contenido sólo existe después de la ejecución de JavaScript.

Raspados sitios web de JavaScript-heavy requiere navegadores sin cabeza — motores de navegador reales corriendo sin una ventana visible que puede ejecutar JavaScript, renderizar DOM, e interactuar con elementos de página. Combinado con proxies, los navegadores sin cabeza desbloquean datos de incluso los sitios web más dinámicos.

Esta guía es parte de nuestra Guía completa de Proxies de Rastreo Web. Para evitar la detección mientras utiliza navegadores sin cabeza, vea Cómo los sistemas anticuerpos detectan proxies.

¿Cuándo necesitas un navegador sin cabeza?

¿Cuándo necesitas un navegador sin cabeza?
EscenarioHTTP simpleNavegador sin cabeza
Páginas HTML estaticasFunciona perfectamente.Overkill
Páginas remitidas por servidor con APIWorks (hit the API directly)No se necesita
SPA (React, Vue, Angular)Consigue concha vacíaNecesidad
Pergamino infinito / carga perezosaNo se puede desencadenarNecesidad
Contenido tras login + JSDificultadRecomendado
Pages with anti-bot JS checksDetección de fallasNecesidad
Compruebe siempre si el sitio tiene una API o renderizado lado servidor antes de llegar a un navegador sin cabeza. Muchos sitios "JavaScript-heavy" realmente tienen puntos finales de API que devuelven JSON limpios — mucho más rápido y más barato para raspar.

Puppeteer + Proxies (Node.js)

Controles de Puppeteer Chrome / Cromo programáticamente. Es la herramienta más madura del navegador sin cabeza para Node.js.

Configuración básica con 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`);

Raspado multipágina optimizado

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();

Playwright + Proxies (Python)

Playwright es una alternativa más nueva que soporta Chromium, Firefox y WebKit. Su API de Python es limpia y bien adaptada para raspar.

Configuración básica

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 for Parallel Scraping

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: Usando cromado con 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))
}

Estrategias de optimización del rendimiento

Los navegadores sin cabeza son 10-50x más lento que las simples peticiones HTTP. Aquí están las estrategias para minimizar la brecha de rendimiento:

1. Bloquear los recursos innecesarios

Imágenes, CSS, fuentes y archivos multimedia no son necesarios para la extracción de datos. Bloquearlas rápidamente cargas de página:

# 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. Use the Right Wait Strategy

2. Use the Right Wait Strategy
EstrategiaSpeedConfiabilidadCaso de uso
domcontentloadedRápidoMay miss async dataPáginas con datos en línea
loadMedianaBien.Más páginas
networkidleDespacio.Más altoEspacios pesados, pergamino infinito
Selector específicoVariableMás altoCuando usted conoce el elemento objetivo

3. Instancias de navegador de reutilización

Lanzamiento de un navegador lleva 1-3 segundos. Para el raspado por lotes, lanzar una vez y crear nuevas páginas/contextos para cada 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. Interceptar llamadas de API en lugar de Parsing DOM

Muchos SPAs recogen datos de API. Interceptar esas llamadas de API directamente — se limpia JSON sin analizar 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`);

Navegador sin cabeza vs HTTP comparación

Navegador sin cabeza vs HTTP comparación
métricaHTTP simple + ProxyNavegador sin cabeza + Proxy
Velocidad por página0,5-2 segundos3-15 segundos
Memoria por ejemplo~50 MB200 a 500 MB
Uso de CPUMinimalSignificado
Ancho de banda por página50-200 KB2 a 10 MB (con recursos)
Reproducción de JavaScriptNoTotal
Antibot bypassLimitedMejor (Navegador real)
Páginas concurrentes100+3-10 por máquina

Buenas prácticas

  • Siempre prueba HTTP primero. Compruebe los puntos finales de API, el contenido de servidor o JSON incrustado en el HTML antes de utilizar un navegador sin cabeza.
  • Bloquear recursos innecesarios. Imágenes, CSS y fuentes agregan tiempo de carga sin proporcionar datos.
  • Use selectores específicos para esperar. networkidle es seguro pero lento. Espera el elemento específico que necesitas.
  • Reutilizar las instancias del navegador. Inicie una vez, cree nuevos contextos por página.
  • Interceptar llamadas API. Muchos SPAs cargan datos a través de APIs — interceptan directamente al JSON.
  • Limite la concurrencia. Los navegadores sin cabeza son intensivos en memoria. 3-5 páginas concurrentes por GB de RAM es una buena regla.
  • Use proxies residenciales. ProxyHat proxies residenciales proporcionar los puntajes de confianza más altos, reduciendo la detección cuando se ejecutan navegadores sin cabeza.

Para manejar CAPTCHAs que los navegadores sin cabeza encuentran, vea Handling CAPTCHAs Cuando Scraping. Para escalar el navegador sin cabeza raspando, leer Cómo escalar infraestructura de cambio.

Empieza con el Python SDK, Nodo SDKo Go SDK para la integración proxy, y explorar ProxyHat for Web Scraping.

Preguntas frecuentes

¿Siempre necesito un navegador sin cabeza para sitios JavaScript?

No. Muchos sitios con JavaScript cargan datos desde los puntos finales de API. Compruebe la pestaña Red del navegador para las solicitudes XHR/fetch — si los datos provienen de una API, puede llamar a esa API directamente con simples peticiones HTTP a través de un proxy, que es mucho más rápido.

Puppeteer o Playwright - ¿Qué es mejor para raspar?

Playwright generalmente se recomienda para nuevos proyectos. Soporta varios motores del navegador (Chromium, Firefox, WebKit), tiene un mejor soporte de asinc autóctono en Python y configuración de proxy integrada. Puppeteer es más maduro y tiene un ecosistema más grande si usted está en el mundo Node.js.

¿Cuántas páginas de navegador sin cabeza puedo ejecutar simultáneamente?

Cada página consume 200-500 MB de RAM. En una máquina con 8 GB de RAM, 3-10 páginas concurrentes es realista. Use bloqueo de recursos (imagenes, CSS) para reducir la memoria. Para mayor concurrencia, distribuya a través de múltiples máquinas utilizando una arquitectura basada en cola.

¿Por qué utilizar proxies con navegadores sin cabeza?

Incluso con un navegador real, las solicitudes repetidas de la misma IP se bloquean. Los ejes rotan su IP para que cada carga de página aparezca proveniente de un usuario diferente. Los proxies residenciales a través de ProxyHat proporcionan los puntajes de confianza más altos, minimizando bloques y CAPTCHAs.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog