Volver al blog

Cómo construir un raspador web autosanador

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

05-Aug-2026

Resumen:

  • Un selector CSS que funciona hoy deja de hacerlo cuando un sitio renombra una clase, y un scraper normal responde silenciosamente devolviendo nada.
  • Un scraper autorreparador repara el selector en tiempo de ejecución: obtiene la página, prueba el selector guardado y, en caso de no coincidir, solicita a un modelo de lenguaje uno nuevo leído desde el DOM en vivo.
  • Este pipeline obtiene la página renderizada a través de la API de Scraping Universal de Scrapeless, así que el modelo siempre se repara contra el marcado real ejecutado por scripts en lugar de un contenedor vacío.
  • Una ejecución en vivo muestra que el selector guardado .author-name no coincide con ningún elemento, luego deepseek-v4-flash propone small.author, que extrae los diez autores.
  • El selector reparado se valida contra el DOM antes de confiar en él: un intento incorrecto no coincide con nada y se descarta, por lo que una mala sugerencia nunca corrompe silenciosamente tus datos.
  • Obtén una clave de API de Scrapeless en el plan gratuito y ejecuta todo el bucle de principio a fin.

Pipeline a Simple Vista

Un scraper falla no porque el código esté mal, sino porque la página se movió. La solución es tratar el selector como datos que pueden ser regenerados, no como una constante integrada en el origen. Este pipeline hace eso en cinco etapas:

extraer la página renderizada (Scrapeless) → probar el selector guardado → detectar el error → reparar con un modelo de lenguaje → validar y extraer → persistir el selector funcional

Cada etapa a continuación se ejecuta contra la página en vivo. La extracción y la reparación son las dos llamadas que salen de tu máquina; todo lo demás es análisis local que puedes inspeccionar.

Etapa 1: Extraer la página renderizada con Scrapeless

La reparación es solo tan buena como el HTML que le muestres al modelo, así que la extracción debe devolver la página que un navegador construiría, no el contenedor vacío que un sitio renderizado por el cliente envía primero. Esa extracción renderizada es el trabajo de la API de Scraping Universal de Scrapeless. La API de Scraping Universal renderiza la página del lado del servidor con js_render y devuelve el HTML terminado en su campo data. Instala las dos bibliotecas que usa el pipeline:

bash Copy
pip install requests beautifulsoup4

Coloca ambas claves en el entorno: la clave de Scrapeless para la extracción, la clave del modelo para la reparación:

bash Copy
export SCRAPELESS_API_KEY="tu_clave_de_api_scapeless"
export OPENROUTER_API_KEY="tu_clave_de_api_openrouter"

Etapa 2: Probar el selector guardado y detectar la falla

Ejecutar el scraper del mes pasado contra la página de hoy y la falla es silenciosa: el selector no coincide con nada y la extracción devuelve una lista vacía, por lo que un trabajo ingenuo escribe cero filas y reporta éxito. La página de demostración a continuación construye sus citas con JavaScript; el selector guardado .author-name es una clase que el marcado ya no usa, en lugar de un selector que un rediseño ha renombrado:

python Copy
import os, requests
from bs4 import BeautifulSoup

def fetch(url):
    r = requests.post(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": True}},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["data"]

soup = BeautifulSoup(fetch("https://quotes.toscrape.com/js/"), "html.parser")

SELECTOR_GUARDADO = ".author-name"          # funcionaba el mes pasado; el sitio renombró la clase
autores = [e.get_text(strip=True) for e in soup.select(SELECTOR_GUARDADO)]
print(f"selector guardado {SELECTOR_GUARDADO!r} coincidió: {len(autores)}",
      "-> RUIDO, no se extrajo nada" si no autores else "-> ok")

La ejecución hace que la falla silenciosa suene fuerte:

text Copy
selector guardado '.author-name' coincidió: 0 -> RUIDO, no se extrajo nada

Etapa 3: Reparar con un modelo de lenguaje

Cuando el selector guardado no coincide con nada, entrega al modelo el DOM en vivo y pide un reemplazo. El modelo lee la estructura real: las etiquetas, clases y anidaciones definidas por la especificación de Selectores CSS — y devuelve un selector adaptado a la página que tiene delante, no a una página que vio en el entrenamiento. Restringir la respuesta a un objeto JSON mantiene la respuesta como una única cadena de selector en lugar de una explicación:

python Copy
import os, json, requests

def propose_selector(html_fragment, target):
    prompt = (f'Un selector CSS para web-scraping ha fallado. A partir de este fragmento HTML, devuelve JSON '
              f'{{"selector": "<css>"}} para el elemento que contiene el {target}. Devuelve SOLO JSON.\n\n'
              f"HTML:\n{html_fragment}")
    r = requests.post(
        "https://openrouter.ai/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}", "Content-Type": "application/json"},
        json={"model": "deepseek/deepseek-v4-flash",
              "messages": [{"role": "user", "content": prompt}],
              "response_format": {"type": "json_object"}, "temperature": 0},
        timeout=120,
    )
    r.raise_for_status()
    return json.loads(r.json()["choices"][0]["message"]["content"])["selector"]

Enviar un bloque de .quote renderizado es suficiente contexto y mantiene bajo el conteo de tokens — y el costo — pequeño.

Obtén tu clave API en el plan gratuito: app.scrapeless.com

Etapa 4: Validar y extraer

Un selector propuesto es una sugerencia hasta que el DOM lo confirma. Ejecuta el selector reparado contra la misma sopa y cuenta las coincidencias: una verdadera reparación devuelve filas, y un mal intento devuelve cero y se descarta antes de poder escribir datos vacíos. El ciclo completo une las tres llamadas:

python Copy
import os, json, requests
from bs4 import BeautifulSoup

def fetch(url):
    r = requests.post(
        "https://api.scrapeless.com/api/v2/unlocker/request",
        headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
        json={"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": True}},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()["data"]

def propose_selector(html_fragment, target):
    prompt = (f'Un selector CSS para web scraping está roto. Desde este fragmento HTML, devuelve JSON '
              f'{{"selector": "<css>"}} para el elemento que contiene el {target}. Regresa SOLAMENTE JSON.\n\n'
              f"HTML:\n{html_fragment}")
    r = requests.post(
        "https://openrouter.ai/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}", "Content-Type": "application/json"},
        json={"model": "deepseek/deepseek-v4-flash",
              "messages": [{"role": "user", "content": prompt}],
              "response_format": {"type": "json_object"}, "temperature": 0},
        timeout=120,
    )
    r.raise_for_status()
    return json.loads(r.json()["choices"][0]["message"]["content"])["selector"]

def extract(soup, selector):
    return [e.get_text(strip=True) for e in soup.select(selector)]

soup = BeautifulSoup(fetch("https://quotes.toscrape.com/js/"), "html.parser")
authors = extract(soup, ".author-name")

if not authors:
    print("selector guardado '.author-name' coincidió: 0 -> reparación")
    sample = str(soup.select_one(".quote") or soup.body)[:1500]
    healed = propose_selector(sample, "nombre del autor de la cita")
    print("selector propuesto por el modelo:", repr(healed))
    authors = extract(soup, healed)
    print("selector reparado coincidió:", len(authors))

print("autores:", ", ".join(authors[:3]), "...")

El selector roto se repara y la extracción se llena de nuevo:

text Copy
selector guardado '.author-name' coincidió: 0 -> reparación
selector propuesto por el modelo: 'small.author'
selector reparado coincidió: 10
autores: Albert Einstein, J.K. Rowling, Jane Austen ...

Debido a que el selector reparado se verifica contra el motor de selectores CSS que tu parser ya usa, la validación es exacta: o coincide con elementos o no, y solo se acepta una coincidencia.

Etapa 5: Persistir el selector funcional

Reparar una vez es reparación; reparar en cada ejecución es un desperdicio. Después de que un selector se valida, escríbelo de vuelta a una pequeña caché indexada por campo, para que la próxima ejecución lea el selector funcional directamente desde el disco y solo llame al modelo cuando ese también falle:

python Copy
import json
from pathlib import Path

CACHE = Path("selectors.json")

def remember(field, selector):
    cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
    cache[field] = selector
    CACHE.write_text(json.dumps(cache, indent=2))

remember("quote_author", "small.author")   # la próxima ejecución comienza desde el selector reparado

El scraper ahora se degrada de manera elegante: se apoya en selectores en caché mientras funcionan y los repara en el momento en que una página cambia, en lugar de fallar hasta que alguien lo note y edite el código.

Conclusión

Un scraper autocompensado convierte un selector roto de una interrupción en un evento de tiempo de ejecución. La API de Scraping Universal de Scrapeless proporciona el HTML renderizado que el modelo necesita para razonar, el modelo lee ese DOM en vivo y propone un selector, y un conteo de coincidencias contra el analizador decide si la solución es real antes de que se escriba cualquier dato. La ejecución anterior — .author-name coincidiendo en cero, small.author coincidiendo en diez — es el bucle en miniatura: buscar, detectar, curar, validar, persistir. Dimensiona el volumen de solicitudes que esperas contra la página de precios antes de escalar y lee el manual sobre cómo un navegador analiza HTML en un DOM si quieres entender por qué la búsqueda renderizada es importante. Para una visión más amplia de los modelos de lenguaje en extracción, el explicador sobre qué es un scraper LLM establece el contexto.

Únete a nuestra comunidad para reclamar un plan gratuito y comparar notas con otros desarrolladores que construyen scrapers resilientes: Discord · Telegram.

Preguntas frecuentes

P: ¿Qué se rompe realmente cuando un scraper "se rompe"?

Generalmente el selector, no la lógica. Un sitio lanza un rediseño que renombra una clase o reestructura su marcado, el selector CSS guardado deja de coincidir y la extracción devuelve una lista vacía. La solicitud aún tiene éxito, por lo que la falla es fácil de pasar por alto.

P: ¿Por qué buscar a través de la API de Scraping Universal de Scrapeless en lugar de una solicitud simple?

Porque el modelo solo puede curar con respecto al HTML que se le muestra. Una página renderizada por el cliente devuelve un contenedor vacío a un cliente HTTP básico, por lo que el modelo razonaría sobre un marcado que no tiene datos. La API de Scraping Universal renderiza la página del lado del servidor y devuelve el DOM final, proporcionando al modelo la estructura real a partir de la cual trabajar.

P: ¿Cómo evitas que el modelo invente un selector que no coincide con nada?

Validas antes de confiar. El selector curado se ejecuta contra el DOM analizado y se cuentan las coincidencias; un selector que devuelve cero elementos se descarta en lugar de usarse. El modelo propone, pero el conteo de coincidencias decide.

P: ¿Llama esto al modelo en cada ejecución?

No. Una vez que un selector se valida, se almacena en caché en el disco, indexado por campo, y las ejecuciones posteriores leen directamente el selector en funcionamiento. El modelo solo se llama cuando un selector en caché deja de coincidir, lo que mantiene tanto la latencia como el costo ligados a una ruptura real en lugar de a cada extracción.

P: ¿Es legal raspar de esta manera?

El bucle de curación no cambia lo que se te permite recopilar. Raspa páginas públicas, respeta los términos del sitio y sus directivas para robots, evita datos personales que no tienes base para procesar y mantén las tasas de solicitud moderadas.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar