El Problema del Web Scraping: Recursos que Se Comen tu Servidor

Playwright es potente. Pero cada instancia de navegador consume 200-500MB de RAM. Multiplica eso por 10 scrapers concurrentes y tu servidor colapsa. Browserless resuelve exactamente esto.

Browserless es un servicio headless optimizado que ejecuta Chromium de forma eficiente, con pool de navegadores, gestión de recursos y APIs HTTP simples. Todo sin levantar una GUI.

La diferencia:

Métrica Playwright Solo Browserless + Playwright
RAM por instancia 400MB ~100MB (pooling)
Tiempo de startup 2-3 segundos ~0.1 segundo (pre-warm)
Configuración Manual Pre-configurado
Escalabilidad Lineal (1 proceso = 1 browser) Pool inteligente
Cleanup Manual (memory leaks comunes) Automático

En esta guía vas a aprender a configurar Browserless con Docker, conectarlo a Playwright y crear 5 scrapers prácticos listos para producción.

🎯 Qué Aprenderás

  • Instalación optimizada con Docker Compose
  • Conexión Playwright con código funcional
  • 5 ejemplos de scraping ready-to-use
  • Integración con n8n para automatización
  • Best practices para producción

Por Qué Browserless + Playwright es la Combinación Perfecta

Ventajas sobre Playwright Solo

  • Menos recursos: Pool de navegadores pre-calentados, no levantas/cierras browsers constantemente
  • APIs HTTP simples: Puedes hacer scraping con curl, no necesitas siempre código
  • Optimizado para servidores: Headless puro, sin X11 ni dependencias gráficas
  • Rate limiting built-in: Control de concurrencia automático
  • Metrics y monitoring: Endpoint /metrics para Prometheus
  • Session management: Mantén sesiones entre requests

Ventajas sobre Puppeteer/Selenium

  • Playwright API moderna: Auto-waiting, mejor manejo de SPA, más estable
  • Multi-browser: Chromium, Firefox, WebKit (Puppeteer solo Chrome)
  • Better debugging: Inspector, traces, screenshots automáticos
  • Más rápido: 20-30% más rápido que Puppeteer en benchmarks

Instalación con Docker (5 Minutos)

Opción 1: Docker Run (Quick Test)

docker run -d \
  --name browserless \
  -p 3000:3000 \
  -e "MAX_CONCURRENT_SESSIONS=10" \
  -e "CONNECTION_TIMEOUT=60000" \
  browserless/chrome:latest

Verifica que funciona:

curl http://localhost:3000/json/version

Deberías ver respuesta JSON con versión de Chromium.

Opción 2: Docker Compose (Recomendado para Producción)

Crea docker-compose.yml:

version: '3.8'

services:
  browserless:
    image: browserless/chrome:latest
    container_name: browserless
    ports:
      - "3000:3000"
    environment:
      # Concurrencia
      MAX_CONCURRENT_SESSIONS: 10
      QUEUE_LENGTH: 50

      # Timeouts (en ms)
      CONNECTION_TIMEOUT: 60000
      DEFAULT_LAUNCH_ARGS: '["--disable-dev-shm-usage", "--no-sandbox"]'

      # Seguridad
      TOKEN: "tu_token_super_secreto"

      # Performance
      PREBOOT_CHROME: "true"
      KEEP_ALIVE: "true"

      # Debugging (desactiva en producción)
      DEBUG: "*"

    volumes:
      # Para guardar screenshots/PDFs
      - ./downloads:/workspace

    restart: unless-stopped

    # Límites de recursos
    mem_limit: 2g
    cpus: 2

Levanta el servicio:

docker-compose up -d

Configuraciones Importantes

MAX_CONCURRENT_SESSIONS: Cuántos navegadores simultáneos. Regla de oro: 200MB RAM por sesión. Con 2GB límite → 10 sesiones máximo.

PREBOOT_CHROME: Pre-calienta navegadores. Reduce latencia de primera request pero usa más RAM idle.

TOKEN: Seguridad básica. Todas las requests deben incluir ?token=tu_token.

DEFAULT_LAUNCH_ARGS: Argumentos de Chromium. Los esenciales:

  • --no-sandbox: Necesario en Docker
  • --disable-dev-shm-usage: Usa /tmp en vez de /dev/shm (evita crashes en containers con poca memoria compartida)
  • --disable-gpu: No necesitas GPU en headless

Conectar Playwright con Browserless (Python)

Instalación de Playwright

pip install playwright
playwright install chromium  # Solo necesario localmente, no en producción con Browserless

Código Base de Conexión

from playwright.sync_api import sync_playwright

BROWSERLESS_URL = "ws://localhost:3000?token=tu_token_super_secreto"

def scrape_with_browserless(url):
    with sync_playwright() as p:
        # Conecta a Browserless vía WebSocket
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)

        # Usa contexto para aislar sesiones
        context = browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        )

        page = context.new_page()

        try:
            # Navega con timeout
            page.goto(url, wait_until='networkidle', timeout=30000)

            # Tu scraping aquí
            title = page.title()
            content = page.content()

            return {'title': title, 'html': content}

        finally:
            # Cleanup crucial
            context.close()
            browser.close()

# Uso
result = scrape_with_browserless('https://example.com')
print(result['title'])

Versión con Node.js

const { chromium } = require('playwright');

const BROWSERLESS_URL = 'ws://localhost:3000?token=tu_token_super_secreto';

async function scrapeWithBrowserless(url) {
  const browser = await chromium.connectOverCDP(BROWSERLESS_URL);
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 }
  });

  const page = await context.newPage();

  try {
    await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });

    const title = await page.title();
    const content = await page.content();

    return { title, content };
  } finally {
    await context.close();
    await browser.close();
  }
}

// Uso
scrapeWithBrowserless('https://example.com')
  .then(result => console.log(result.title));

5 Ejemplos Prácticos de Scraping

1. Scraping Básico con Selectores

Caso de uso: Extraer precios y títulos de productos.

def scrape_products(url):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
        page = browser.new_page()
        page.goto(url)

        # Espera a que los productos carguen
        page.wait_for_selector('.product-card')

        # Extrae todos los productos
        products = page.eval_on_selector_all('.product-card', '''
            (elements) => elements.map(el => ({
                title: el.querySelector('h3')?.textContent,
                price: el.querySelector('.price')?.textContent,
                image: el.querySelector('img')?.src,
                link: el.querySelector('a')?.href
            }))
        ''')

        browser.close()
        return products

# Uso
products = scrape_products('https://shop.example.com/products')
for p in products:
    print(f"{p['title']}: {p['price']}")

2. Login y Scraping Autenticado

Caso de uso: Scraping de panel privado después de login.

def scrape_authenticated(login_url, dashboard_url, username, password):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
        context = browser.new_context()
        page = context.new_page()

        # 1. Navega a login
        page.goto(login_url)

        # 2. Rellena formulario
        page.fill('input[name="username"]', username)
        page.fill('input[name="password"]', password)

        # 3. Click en submit y espera navegación
        with page.expect_navigation():
            page.click('button[type="submit"]')

        # 4. Verifica login exitoso
        page.wait_for_selector('.dashboard')

        # 5. Navega a página privada
        page.goto(dashboard_url)

        # 6. Extrae datos
        data = page.eval_on_selector('.stats', '''
            (el) => ({
                revenue: el.querySelector('.revenue')?.textContent,
                users: el.querySelector('.users')?.textContent,
                orders: el.querySelector('.orders')?.textContent
            })
        ''')

        # Guarda cookies para próximas ejecuciones
        cookies = context.cookies()

        browser.close()
        return data, cookies

3. Scraping con Paginación Infinita

Caso de uso: Sitios con scroll infinito (Twitter, Instagram, etc).

def scrape_infinite_scroll(url, max_scrolls=10):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
        page = browser.new_page()
        page.goto(url)

        all_items = []
        previous_height = 0
        scroll_count = 0

        while scroll_count < max_scrolls:
            # Extrae items actuales
            items = page.eval_on_selector_all('.item', '''
                (elements) => elements.map(el => ({
                    title: el.querySelector('.title')?.textContent,
                    content: el.querySelector('.content')?.textContent
                }))
            ''')

            all_items.extend(items)

            # Scroll hasta el final
            page.evaluate('window.scrollTo(0, document.body.scrollHeight)')

            # Espera a que cargue nuevo contenido
            page.wait_for_timeout(2000)

            # Verifica si hay nuevo contenido
            new_height = page.evaluate('document.body.scrollHeight')
            if new_height == previous_height:
                # No hay más contenido
                break

            previous_height = new_height
            scroll_count += 1

        browser.close()

        # Deduplica por título
        unique_items = {item['title']: item for item in all_items}.values()
        return list(unique_items)

4. Screenshots y PDFs

Caso de uso: Generar capturas o PDFs de páginas web.

def generate_screenshot_and_pdf(url, output_dir='./downloads'):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
        page = browser.new_page()
        page.goto(url, wait_until='networkidle')

        # Screenshot completo
        page.screenshot(
            path=f'{output_dir}/screenshot.png',
            full_page=True
        )

        # Screenshot de elemento específico
        page.locator('#main-content').screenshot(
            path=f'{output_dir}/element.png'
        )

        # PDF (solo funciona en Chromium headless)
        page.pdf(
            path=f'{output_dir}/page.pdf',
            format='A4',
            print_background=True
        )

        browser.close()

        return {
            'screenshot': f'{output_dir}/screenshot.png',
            'pdf': f'{output_dir}/page.pdf'
        }

# Uso
files = generate_screenshot_and_pdf('https://example.com/report')
print(f"Guardados: {files}")

5. Manejo de JavaScript Dinámico

Caso de uso: SPAs con contenido que carga vía JavaScript.

def scrape_spa(url):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
        page = browser.new_page()

        # Intercepta requests para acelerar (bloquea imágenes, fonts)
        page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', lambda route: route.abort())

        page.goto(url)

        # Espera a que el contenido dinámico cargue
        page.wait_for_selector('[data-loaded="true"]', timeout=10000)

        # O espera a que desaparezca el loader
        page.wait_for_selector('.loader', state='hidden')

        # O espera condición custom
        page.wait_for_function('''
            () => {
                return window.appReady === true &&
                       document.querySelectorAll('.item').length > 0
            }
        ''', timeout=15000)

        # Extrae datos
        data = page.evaluate('''
            () => {
                // Accede a variables JavaScript de la SPA
                return {
                    state: window.__APP_STATE__,
                    items: Array.from(document.querySelectorAll('.item')).map(el => ({
                        id: el.dataset.id,
                        title: el.textContent
                    }))
                }
            }
        ''')

        browser.close()
        return data

Integración con n8n para Automatización

Browserless se integra perfectamente con n8n para scraping automatizado. Ya tenemos un tutorial completo: Automatización Web con n8n, Browserless y Playwright.

Workflow típico en n8n:

  1. Cron node → Ejecuta cada hora
  2. HTTP Request node → Conecta a Browserless
  3. Code node → Parsea HTML scrapeado
  4. Database node → Guarda resultados
  5. Telegram node → Notifica si hay cambios

Código del HTTP Request node:

Method: POST
URL: http://browserless:3000/content?token={{$env.BROWSERLESS_TOKEN}}
Headers:
  Content-Type: application/json
Body:
{
  "url": "https://target-site.com",
  "waitFor": ".main-content",
  "gotoOptions": {
    "waitUntil": "networkidle"
  }
}

Más workflows en: Guía Completa de n8n v2.

Best Practices para Producción

1. Manejo de Errores Robusto

def scrape_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        try:
            with sync_playwright() as p:
                browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
                page = browser.new_page()

                # Set timeout
                page.set_default_timeout(30000)

                page.goto(url)
                data = page.content()

                browser.close()
                return data

        except TimeoutError:
            print(f"Timeout en intento {attempt + 1}")
            if attempt < max_retries - 1:
                time.sleep(5 * (attempt + 1))  # Exponential backoff
            else:
                raise

        except Exception as e:
            print(f"Error: {e}")
            if attempt == max_retries - 1:
                raise

2. Rate Limiting

import time
from collections import deque

class RateLimiter:
    def __init__(self, max_requests, time_window):
        self.max_requests = max_requests
        self.time_window = time_window
        self.requests = deque()

    def wait_if_needed(self):
        now = time.time()

        # Elimina requests antiguas
        while self.requests and self.requests[0] < now - self.time_window:
            self.requests.popleft()

        # Si llegamos al límite, espera
        if len(self.requests) >= self.max_requests:
            sleep_time = self.time_window - (now - self.requests[0])
            time.sleep(sleep_time)
            self.requests.popleft()

        self.requests.append(now)

# Uso: max 10 requests por minuto
limiter = RateLimiter(max_requests=10, time_window=60)

for url in urls:
    limiter.wait_if_needed()
    scrape_with_browserless(url)

3. Pooling de Conexiones

class BrowserPool:
    def __init__(self, pool_size=5):
        self.pool_size = pool_size
        self.browsers = []
        self._init_pool()

    def _init_pool(self):
        p = sync_playwright().start()
        for _ in range(self.pool_size):
            browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
            self.browsers.append(browser)

    def get_page(self):
        browser = self.browsers.pop(0)
        page = browser.new_page()
        self.browsers.append(browser)  # Rotate
        return page

    def close_all(self):
        for browser in self.browsers:
            browser.close()

# Uso
pool = BrowserPool(pool_size=5)

for url in urls:
    page = pool.get_page()
    page.goto(url)
    # ... scraping ...
    page.close()

pool.close_all()

4. Monitorización

# Verifica salud de Browserless
def check_browserless_health():
    import requests
    response = requests.get('http://localhost:3000/metrics')

    metrics = response.text
    print(f"Browserless metrics:\\n{metrics}")

    # Parsea métricas Prometheus
    lines = metrics.split('\\n')
    for line in lines:
        if 'browserless_active_sessions' in line:
            active = int(line.split()[-1])
            print(f"Sesiones activas: {active}")

# Ejecuta cada 5 minutos con cron
check_browserless_health()

Troubleshooting Común

1. "Target closed" / Connection Lost

Causa: Navegador cerrado prematuramente o timeout.

Solución:

- Aumenta CONNECTION_TIMEOUT en Browserless
- Asegúrate de hacer browser.close() en finally
- Verifica que no excedes MAX_CONCURRENT_SESSIONS

2. Alta Uso de RAM

Causa: Demasiadas sesiones concurrentes o memory leaks.

Solución:

- Reduce MAX_CONCURRENT_SESSIONS
- Implementa pooling de conexiones
- Usa page.close() después de cada scraping
- Activa PREBOOT_CHROME=false si no necesitas velocidad

3. Scraping Detectado / Bloqueado

Causa: User agent, falta de cookies, comportamiento no-humano.

Solución:

- Usa user_agent real y variado
- Implementa delays random entre acciones
- Usa stealth plugin: playwright-stealth
- Rota IPs si es necesario (proxies)

Conclusión: Scraping Eficiente y Escalable

Browserless + Playwright es la combinación definitiva para scraping en producción. Menos recursos, más estabilidad, mejor rendimiento.

Recapitulación:

  • ✅ 60-70% menos uso de RAM vs Playwright solo
  • ✅ Tiempo de startup reducido a ~0.1s (vs 2-3s)
  • ✅ Pool de navegadores automático
  • ✅ APIs HTTP para scraping sin código
  • ✅ Integración perfecta con n8n para automatización

Siguiente paso: Instala Browserless, copia uno de los 5 ejemplos, adáptalo a tu caso de uso. En 15 minutos tendrás scraping en producción.

Sobre este tutorial: Todos los ejemplos están probados y funcionan en producción. Código basado en Playwright 1.40+ y Browserless latest. Última actualización: Octubre 2025.

Preguntas Frecuentes sobre Browserless y Playwright

¿Browserless es gratis?

Browserless tiene versión open source gratuita (self-hosted con Docker). También ofrecen servicio cloud con free tier limitado. Para homelab/producción propia: completamente gratis con Docker.

¿Cuánta RAM necesita Browserless?

Cada navegador consume ~100-200MB con pooling activado (vs 400MB+ con Playwright solo). Con 2GB RAM puedes ejecutar 10 sesiones concurrentes confortablemente.

¿Browserless funciona con Puppeteer?

Sí, Browserless soporta tanto Playwright como Puppeteer. Usa el mismo endpoint pero cambiando la librería de conexión. API compatible con ambos.

¿Puedo usar Browserless sin Docker?

Técnicamente sí (instalación npm), pero Docker es altamente recomendado. Simplifica dependencias, aislamiento y configuración. Con Docker: 1 comando y funciona.

¿Browserless detecta bots?

Browserless + Playwright es menos detectable que Selenium. Usa navegador real (Chromium). Para máxima evasión: combina con playwright-stealth plugin y user agents rotatorios.

¿Cómo escalo Browserless?

Múltiples contenedores Browserless detrás de load balancer. Cada contenedor maneja N sesiones. Escala horizontal: 3 containers × 10 sesiones = 30 scrapers concurrentes.

Por ziru

El Diario IA
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.