Agentes de LangChain que ven la web en vivo: Construyendo pipelines de datos de IA con Scrapeless
Web Data Collection Specialist
Puntos Clave:
- Integración de LangChain de primera mano. El paquete
langchain-scrapelessen PyPI incluye cinco herramientas listas para usar:ScrapelessDeepSerpGoogleSearchTool,ScrapelessDeepSerpGoogleTrendsTool,ScrapelessUniversalScrapingTool,ScrapelessCrawlerCrawlToolyScrapelessCrawlerScrapeTool— que se integran directamente en un agente de LangChain o LangGraph. Sin necesidad de plomería de subprocessos, ni cableado CDP personalizado. - El pipeline consta de cuatro movimientos: Descubrir → Renderizar → Extraer → Almacenar. Busca en Google con la herramienta deep-SERP, renderiza cualquier URL con la herramienta universal de scraping (Scraping Browser bajo el capó), extrae registros tipados con
PydanticOutputParser, y opcionalmente incrusta en un almacén vectorial para RAG descendente. - El agente decide cuándo raspar. La función
create_agentde LangChain (runtime de LangGraph bajo el capó) permite que el LLM elija entre responder basado en contexto previo y llamar a la herramienta Scrapeless — el navegador en la nube solo se activa cuando realmente se necesitan datos frescos, lo que mantiene bajo el costo y la latencia en giros rutinarios. - Registros tipados, no HTML crudo. Emparejar
ScrapelessUniversalScrapingTool(respuesta en markdown) con un esquema Pydantic convierte las páginas raspadas en registros validados deProducto/Artículo/OfertaLaboralde los cuales el código descendente puede fiarse sin necesidad de pegamento de análisis adicional. - Navegador en la nube anti-detección, proxies residenciales en más de 195 países. Scrapeless Scraping Browser maneja la renderización de JavaScript, egress de proxies residenciales y aleatorización de huellas digitales (UA, zona horaria, WebGL, canvas) en cada sesión, por lo que el agente se mantiene centrado en el razonamiento en lugar de en plomería de evasión.
Introducción: Pipelines de datos de IA que ven la web en vivo
Un LLM desnudo responde a partir de datos de entrenamiento. Para la mayoría de los flujos de trabajo agentes que realmente se implementan — inteligencia competitiva de precios, investigación de leads, monitoreo de mercados, ingestión estructurada de noticias, RAG sobre la web en vivo — el modelo necesita ver la página ahora mismo. Los cortes de entrenamiento, muros de pago, SPAs cargadas de forma perezosa y renderización personalizada empujan la respuesta a un territorio que el LLM nunca vio, y la respuesta ya sea cita un número desactualizado o se niega educadamente.
LangChain más un navegador en la nube es la respuesta estándar. El modelo razona; el navegador busca; el agente une los dos. El punto de fricción que la mayoría de los equipos encuentra está por debajo del agente: proxies residenciales, renderización de JavaScript, huellas digitales anti-detección y ciclo de vida de sesión cada uno necesita ser resuelto antes de que el agente pueda hacer algo útil. Playwright directo sobre un VPN residencial funciona para una sola ejecución en laptop; no sobrevive a un cronograma de producción.
Scrapeless Scraping Browser maneja esas cuatro preocupaciones a nivel de plataforma, y el paquete langchain-scrapeless en PyPI las expone como herramientas nativas de LangChain. Esta publicación recorre la composición de esas herramientas en un pipeline de datos de IA en cuatro pasos Descubrir → Renderizar → Extraer → Almacenar, con un ejemplo de investigación competitiva trabajado, salida tipada de Pydantic, concurrencia acotada y ganchos de observabilidad. Para el mismo primitivo sobre un protocolo diferente, consulta la publicación de integración MCP.
Qué puedes construir
Las cinco herramientas enviadas por langchain-scrapeless cubren los patrones más comunes de pipelines de datos de IA:
- Inteligencia competitiva de precios. Busca en una categoría, renderiza las páginas de los principales minoristas, extrae un registro
Productotipado con precio, calificación y conteo de reseñas. - Monitoreo de SERP. Rastrear la clasificación de palabras clave y el cambio de fragmentos a través de regiones con
ScrapelessDeepSerpGoogleSearchToolparametrizado porglyhl. - Seguimiento de tendencias de mercado. Extrae
interest_over_timey consultas relacionadas conScrapelessDeepSerpGoogleTrendsToolpara dimensionar categorías (el acceso al endpoint de Trends depende de tu nivel de plan de Scrapeless). - Extracción de detalles de productos a escala. Aliméntate de una lista de URL en
ScrapelessCrawlerScrapeTooly obtén markdown listo para un extractor LLM. - Generación de leads a partir de directorios. Rastrea sitios de listados comerciales con
ScrapelessCrawlerCrawlTool, analiza filas de contacto en registros tipados y elimina duplicados por dominio. - Ingestión estructurada de noticias para RAG. Renderiza páginas de editores a markdown limpio, extrae registros de
Artículo, incrusta en un almacén vectorial de LangChain y consulta con cadenas aumentadas por recuperación.
Todos los seis pipelines componen los mismos primitivos — buscar, renderizar, extraer, almacenar — y el ejemplo trabajado a continuación cubre toda la cadena de principio a fin.
Por qué Scrapeless Scraping Browser
Scrapeless Scraping Browser es un navegador en la nube anti-detección y personalizable, diseñado para crawlers web y agentes de IA. Para los agentes de LangChain específicamente, trae:
- Proxies residenciales en más de 195 países — las consultas geolocalizadas devuelven los listados que un usuario local vería, y la rotación es automática en cada sesión.
- Renderización de JavaScript en el lado de la nube: Chromium completo con la página hidratada antes de la extracción, por lo que las SPA, los feeds de desplazamiento infinito y los paneles cargados de forma diferida son objetivos de primera clase.
- Huella de anti-detección en cada sesión: UA, zona horaria, idioma, resolución de pantalla, WebGL y canvas son aleatorizados por sesión, con una API de huella personalizada para identidades fijas cuando la consistencia es importante.
- Persistencia de sesión en la capa del navegador en la nube a través de
sessionTTL(60–900s) ysessionName: disponibles al dirigir el endpoint WSS directamente; las llamadas a la herramientalangchain-scrapelessasignan una nueva sesión porinvoke, que es el valor predeterminado adecuado para un pipeline de investigación. - Integración de LangChain de primera parte:
pip install langchain-scrapelessexpone el navegador en la nube como herramientas nativas de LangChain; sin envolturas de subprocesos, sin plomería CDP, sin serializadores personalizados.
Obtén tu clave API en el plan gratuito en Scrapeless. La integración completa está documentada en github.com/scrapeless-ai/langchain-scrapeless.
Reclama tu plan gratuito y comienza a raspar:
Únete a la vibrante comunidad de Scrapeless para reclamar un plan gratuito de $5-10 y conectarte con otros innovadores:
Comunidad Oficial de Discord de Scrapeless
Comunidad Oficial de Telegram de Scrapeless
Requisitos previos
- Python 3.10 o posterior.
- Una cuenta de Scrapeless y clave API: regístrate en Scrapeless y copia la clave de Configuración → Gestión de Claves API.
- Una clave API de modelo de chat: los ejemplos a continuación utilizan OpenAI (
OPENAI_API_KEY); el mismo código de agente funciona conlangchain-anthropic,langchain-google-genai,langchain-ollama, o cualquier modelo de chat de LangChain simplemente intercambiando la líneaChatOpenAI. - Familiaridad básica con
pipyvenv.
Instalación
La configuración completa consta de cuatro sub-pasos. Cada uno es verificable de forma independiente, por lo que puedes pausar y confirmar antes de continuar.
1. Crea un entorno virtual y instala los paquetes
bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install langchain langchain-scrapeless langchain-openai langgraph pydantic
langchain-scrapeless incluye langchain-core y el SDK de Python de Scrapeless como dependencias transitivas. El meta-paquete langchain proporciona langchain.agents.create_agent, el runtime moderno (no obsoleto) de agente ReAct; langgraph proporciona el runtime subyacente de CompiledStateGraph; langchain-openai es el proveedor del modelo de chat utilizado en los ejemplos. Cambia a langchain-anthropic u otro proveedor si lo prefieres.
2. Configura tus claves API
Exporta ambas claves para la sesión actual del shell:
bash
export SCRAPELESS_API_KEY="tu_token_api_aqui"
export OPENAI_API_KEY="tu_token_openai_aqui"
Para una instalación permanente, agrega las mismas líneas a ~/.bashrc / ~/.zshrc, o utiliza un cargador .env (python-dotenv) y carga el archivo al iniciar el proceso. Las herramientas de Scrapeless leen SCRAPELESS_API_KEY del entorno automáticamente; no lo pases como un argumento de constructor.
3. Verifica la instalación
Una breve prueba de humo que ejercita la herramienta de búsqueda e imprime un resultado en una línea. La API de Scrapeless ocasionalmente devuelve un 400 transitorio en la primera llamada después de un inicio en frío; la prueba se repite hasta tres veces, por lo que un solo intento de reintento resuelve esos casos:
python
# verify.py
import time
from langchain_scrapeless import ScrapelessDeepSerpGoogleSearchTool
tool = ScrapelessDeepSerpGoogleSearchTool()
for attempt in range(3):
try:
result = tool.invoke({"q": "navegador de raspado sin esfuerzo",
"hl": "es", "gl": "mx"})
print(str(result)[:300])
break
except ValueError as e:
print(f"transitorio (intento {attempt + 1}): {e}")
time.sleep(3)
Ejecuta: python verify.py. Una ejecución exitosa imprime una cadena de resultados de búsqueda en unos pocos segundos. Si los tres intentos provocan ValueError con falló con estado 401, la clave API falta o es incorrecta; verifica de nuevo con echo $SCRAPELESS_API_KEY en el mismo shell. Un 400 persistente después de tres intentos indica que la cuenta puede no tener acceso al endpoint solicitado; consulta las preguntas frecuentes sobre errores transitorios.
4. (Opcional) Fija las versiones de las dependencias
Para construcciones reproducibles, fija las versiones con las que has probado. La combinación resuelta por pip install langchain langchain-scrapeless langchain-openai langgraph pydantic en un entorno limpio de Python 3.12 es:
langchain==1.2.17
langchain-core==1.3.2
langchain-openai==1.2.1
langchain-scrapeless==0.1.3
langgraph==1.1.10
pydantic==2.13.3
scrapeless==1.1.1
Nota: los metadatos del paquete de langchain-scrapeless 0.1.3 declaran langchain-core <0.4.0, pero pip resuelve a langchain-core 1.3.2 porque langchain-openai lo requiere; la importación en tiempo de ejecución aún funciona. Para evitar completamente la advertencia del resolvedor, instala langchain-scrapeless y solo el proveedor de modelo de chat que necesites (por ejemplo, elimina langchain-openai y pasa una instancia de ChatAnthropic en su lugar). LangGraph 1.0 se puso en producción en octubre de 2025; la serie 1.1.x es la actual estable.
Cómo usar esto: envía un mensaje a tu agente
Después de la instalación, construyes tuberías hablando con el agente, no reinventando selectores CSS cada vez que el sitio objetivo rota su DOM. El agente se encarga del ciclo de descubrir → renderizar → extraer y el LLM selecciona la herramienta adecuada para cada turno.
Prompts que puedes pegar
| Escribes | Lo que hace el agente |
|---|---|
| "Encuentra las 5 mejores máquinas de espresso portátiles y devuelve nombre, precio, calificación como JSON." | Busca en Google, renderiza los mejores resultados, extrae un Product[] tipado. |
"Dame el interés actual de Google Trends durante los últimos 12 meses para base de datos vectorial en EE.UU." |
Llama a ScrapelessDeepSerpGoogleTrendsTool con data_type="interest_over_time" (requiere acceso a nivel de plan a la API de Trends). |
"Raspa https://example.com/docs hasta una profundidad de 2 y devuelve markdown para cada página." |
Llama a ScrapelessCrawlerCrawlTool con limit=.... |
"Renderiza https://www.amazon.com/dp/B08N5WRWNW como markdown." |
Llama a ScrapelessUniversalScrapingTool con response_type="markdown". |
"Busca startups Serie A en fintech 2026, lista las empresas y el tamaño de su ronda de financiación." |
Búsqueda → renderizado → extracción tipada en cadena automáticamente. |
| "Extrae la página de inicio y la página de precios de estos tres competidores SaaS y resume las diferencias." | Multi-URL ScrapelessCrawlerScrapeTool → resumen LLM. |
"Monitorea esta página de carreras de Greenhouse y dímelo qué roles coinciden con ingeniero de personal o infra." |
Renderiza → filtro de palabras clave → filas JSON. |
"¿Cuáles son los 10 mejores resultados orgánicos para tutorial de langchain scrapeless desde una salida en el Reino Unido?" |
ScrapelessDeepSerpGoogleSearchTool con gl="uk", hl="en". |
Ejemplo práctico
Escribes:
Encuentra las 3 mejores máquinas de espresso portátiles para viajar por menos de $150. Para cada una, devuelve nombre, precio, calificación promedio, cantidad de reseñas y tres características clave. Limita la búsqueda a una salida en EE.UU.
El plan del agente (en inglés sencillo):
- Llama a
ScrapelessDeepSerpGoogleSearchToolconq="mejores máquinas de espresso portátiles bajo 150",gl="us",hl="en",num=5para obtener URLs de resultados orgánicos. - Para las 3 mejores URLs de resultados, llama a
ScrapelessUniversalScrapingToolconresponse_type="markdown"para renderizar cada página como markdown limpio. - Pasa cada página renderizada al LLM con un
PydanticOutputParservinculado a un esquemaProduct; rechaza cualquier página donde el analizador no logre extraernombreyprecio. - Agrega los tres registros
Producten un array JSON y devuélvelos.
Lo que obtienes a cambio:
json
[
{
"name": "Wacaco Nanopresso",
"price": 79.95,
"rating": 4.7,
"review_count": 12483,
"key_features": [
"Operación manual con bomba de mano",
"Hasta 18 bares de presión de extracción",
"Compatible con café molido o cápsulas NS a través de adaptador"
],
"url": "https://example.com/p/wacaco-nanopresso"
},
{
"name": "Flair NEO Flex",
"price": 119.00,
"rating": 4.5,
"review_count": 2104,
"key_features": [
"Accionado por palanca, no requiere electricidad",
"Presión de calidad de espresso con portafiltro sin fondo",
"Desmontable para viajar"
],
"url": "https://example.com/p/flair-neo-flex"
},
{
"name": "Outin Nano",
"price": 129.99,
"rating": 4.6,
"review_count": 5871,
"key_features": [
"Elemento calefactor integrado",
"Ciclo de autolimpieza",
"Carga USB-C, ~3 minutos de calentamiento"
],
"url": "https://example.com/p/outin-nano"
}
]
// El esquema refleja exactamente lo que emite el analizador del Paso 4. Los valores de los campos son ilustrativos.
Moldeando prompts
| Frase | Efecto |
|---|---|
"Usa una salida alemana (gl=de)." |
Determina la región del proxy de la herramienta de búsqueda; los resultados devuelven lo que vería un usuario en Berlín. |
| "Extrae como markdown." | response_type="markdown" en la herramienta de raspado universal — contexto LLM más económico, más estable en selectores que HTML. |
| "Limita la búsqueda a 25 páginas." | limit=25 en ScrapelessCrawlerCrawlTool. |
| "Salta páginas que no tienen precio." | El analizador devuelve None para campos faltantes; el agente filtra. |
| "Ejecuta tres URL en paralelo." | Transferencia al patrón de concurrencia limitada en el Paso 6 a continuación. |
Los Pasos 1–6 a continuación son la referencia bajo el capó. Léelos una vez para ver cómo se compone el patrón de descubrir → renderizar → extraer → almacenar; luego confía en que el agente lo aplique a cualquier consulta que el operador transfiera.
Arquitectura
┌──────────────────────────────────────────────────────────────────────┐
│ LangChain create_agent (ejecución de LangGraph) │
│ │
│ ┌───────────────────────┐ ┌────────────────────────────────┐ │
│ │ Modelo de chat │ ──► │ Herramientas (langchain-scrapeless) │ │
│ │ (OpenAI / Anthropic │ │ • BúsquedaGoogleDeepSerp │ │
│ │ / Gemini / Ollama) │ ◄── │ • RaspadoUniversal │ │
│ └───────────────────────┘ │ • RastreadorRastrear │ │
│ ▲ │ • RastreadorRaspar │ │
│ │ │ • TendenciasGoogleDeepSerp │ │
│ │ └──────────────┬─────────────────┘ │
│ │ │ │
│ │ PydanticOutputParser │ │
│ │ (registros tipados) ▼ │
│ │ ┌──────────────────────────────────┐ │
│ │ │ Navegador en la nube sin raspar │ │
│ │ │ • proxies residenciales (195+) │ │
│ │ │ • huella anti detección │ │
│ │ │ • renderizado JS de Chromium │ │
│ │ │ • duración de sesión 60–900s │ │
│ │ └──────────────────────────────────┘ │
│ │ │ │
│ └──────────────────────────────────┘ │
│ los registros tipados fluyen de vuelta al agente │
└──────────────────────────────────────────────────────────────────────┘
│
▼ (opcional)
┌───────────────────────────┐
│ Almacenamiento vectorial (Chroma / │
│ PGVector / pgvector) │
└───────────────────────────┘
Tres capas, separación limpia: el LLM razona sobre la conversación, las herramientas langchain-scrapeless envuelven el navegador en la nube a través de interfaces nativas de LangChain, y el navegador en la nube maneja cada preocupación que no es razonamiento. Cada capa puede ser intercambiada: modelo de chat, aviso, incluso la herramienta subyacente, sin reescribir las demás.
Paso 1 — Definir el esquema de salida tipada
Pydantic es el elemento básico que convierte el markdown raspado en algo en lo que el código de abajo puede confiar. Define el registro objetivo una vez y enlázalo con el extractor LLM en el Paso 4.
python
# schema.py
from typing import Optional
from pydantic import BaseModel, Field, HttpUrl
class Producto(BaseModel):
nombre: str = Field(...,
description="Nombre del producto. Siempre requerido — utiliza el título de la página o H1 si no hay un nombre claro del producto.")
precio: Optional[float] = Field(None, description="Precio numérico en USD; null si está ausente")
calificación: Optional[float] = Field(None, description="Calificación promedio, 0–5; null si está ausente")
conteo_opiniones: Optional[int] = Field(None, description="Número de opiniones; null si está ausente")
características_clave: list[str] = Field(default_factory=list, description="3–5 viñetas cortas de características")
url: HttpUrl = Field(..., description="URL canónica del producto")
Marca todos los campos que pueden estar ausentes como Optional y las listas predeterminadas como vacías; intersticiales anti-bot, diferencias en el diseño regional y nodos DOM cargados de forma perezosa significan que las páginas rutinariamente omiten uno o dos campos, y un esquema no opcional rechaza filas que de otro modo serían útiles. Mantén nombre como requerido y dale un respaldo en su descripción (título de la página, H1) para que el extractor nunca devuelva null para el campo requerido; esa única pista permite que el esquema absorba páginas ruidosas sin levantar ninguna excepción.
Paso 2 — Descubrir con HerramientaBúsquedaGoogleDeepSerpSinRaspar
La herramienta de SERP profunda devuelve resultados orgánicos de Google para una consulta, parametrizada por idioma (hl), país (gl) y cantidad de resultados (num). Es el elemento de descubrimiento: la búsqueda amplía el universo de URLs antes de que comprometas cualquier presupuesto de renderizado por página.
python
# discover.py
from langchain_scrapeless import ScrapelessDeepSerpGoogleSearchTool
search = ScrapelessDeepSerpGoogleSearchTool()
results = search.invoke({
"q": "mejores máquinas de espresso portátiles 2026 por debajo de 150",
"hl": "es",
"gl": "es",
"num": 5,
})
print(results)
hl controla el idioma del resultado y gl controla el país de salida; son los controladores regionales. Para el monitoreo de SERP a través de regiones, ejecuta la misma consulta con diferentes valores gl (us, de, jp, br) y compara las listas de resultados. Respuestas transitorias de ValueError (HTTP 400/503 envueltas por la herramienta) o TimeoutError son normales en el alto volumen de consultas; envuelve la llamada con el decorador de reintento del Paso 6 antes de escalar.
Paso 3 — Renderizar con HerramientaRaspadoUniversalSinRaspar
La herramienta de raspado universal es el Navegador de Raspado Sin Rastro. Acepta una URL y devuelve la página renderizada como markdown (o HTML, o captura de pantalla). Markdown es el formato más económico para alimentar a un extractor de LLM: elimina anuncios, elementos de navegación y estilos en línea, dejando solo el contenido del que realmente trata la página.
python
# render.py
from langchain_scrapeless import ScrapelessUniversalScrapingTool
scrape = ScrapelessUniversalScrapingTool()
markdown = scrape.invoke({
"url": "https://example.com/p/wacaco-nanopresso",
"response_type": "markdown",
})
print(markdown[:600])
Cada invoke asigna una nueva sesión de navegador en la nube, que es el predeterminado correcto para un pipeline de investigación: sesiones nuevas por URL son más simples y más resistentes a los estados anti-bot por sesión. (La reutilización de sesión a través de sessionName es una característica a nivel de CDP; si tu flujo de trabajo necesita cookies activas y estado de inicio de sesión a través de páginas, controla el navegador en la nube directamente a través del endpoint WSS en lugar de a través de esta herramienta de LangChain). La herramienta también acepta response_type="html" cuando necesitas ejecutar tus propios selectores, response_type="plaintext" para el contexto de LLM más económico, o response_type="png" / "jpeg" para pipelines de regresión visual.
Paso 4 — Extraer con PydanticOutputParser
El esquema vinculado en el Paso 1 se conecta directamente a una cadena de Lenguaje de Expresión de LangChain (LCEL) que toma markdown renderizado y devuelve un Producto tipado. El analizador inyecta el esquema JSON en el aviso y valida la respuesta del LLM contra él.
python
# extract.py
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from schema import Product
parser = PydanticOutputParser(pydantic_object=Product)
prompt = ChatPromptTemplate.from_messages([
("system",
"Extraes registros de productos de páginas web renderizadas.\n"
"La salida coincide estrictamente con este esquema:\n{format_instructions}"),
("human",
"URL fuente: {url}\n\nMarkdown renderizado:\n{markdown}\n\n"
"Devuelve un registro de Producto. El campo `name` es obligatorio: usa el título de la página o el H1 si no hay un nombre de producto claro. Establece SOLO los campos opcionales (precio, calificación, conteo de reseñas) a null cuando estén ausentes."),
]).partial(format_instructions=parser.get_format_instructions())
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
extract_chain = prompt | llm | parser
product = extract_chain.invoke({"url": "https://example.com/p/wacaco-nanopresso",
"markdown": "<markdown renderizado del Paso 3>"})
print(product.model_dump())
Las fallas del analizador son raras cuando se cumplen tres cosas: (1) cada campo incierto está marcado como Optional[...], (2) el aviso le dice explícitamente al modelo que solo los campos opcionales pueden ser null, y (3) cada campo requerido tiene una solución en su descripción (por ejemplo, name usa el título de la página o H1). Con esas tres condiciones en su lugar, el contrato nullable del esquema maneja los campos opcionales que faltan, y la instrucción de aviso mantiene al LLM de anular campos requeridos en páginas ruidosas, por lo que la cadena se ejecuta correctamente sin ningún contenedor de reintento.
Paso 5 — Componer en un create_agent
El agente une las tres herramientas y permite que el LLM decida cuál llamar para cualquier mensaje de usuario dado. langchain.agents.create_agent es el runtime canónico desde langchain 1.2 (reemplaza al obsoleto langgraph.prebuilt.create_react_agent y utiliza la misma gráfica de estado de LangGraph bajo el capó).
python
# agent.py
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain_scrapeless import (
ScrapelessDeepSerpGoogleSearchTool,
ScrapelessUniversalScrapingTool,
ScrapelessCrawlerCrawlTool,
)
from tenacity import (retry, stop_after_attempt,
wait_exponential, retry_if_exception_type)
# Herramientas de primer nivel subyacentes
_search = ScrapelessDeepSerpGoogleSearchTool()
_scrape = ScrapelessUniversalScrapingTool()
_crawl = ScrapelessCrawlerCrawlTool()
# El decorador de reintento que el agente verá en cada llamada de herramienta.
# Las herramientas Scrapeless exponen errores de API transitorios como ValueError, así que ese es el filtro.
_retry = retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=15),
retry=retry_if_exception_type(ValueError),
)
def _check(payload: str) -> str:
# El navegador en la nube a veces devuelve HTTP 200 con un JSON ERR_ embebido.
# Supéralo como ValueError para que el decorador @_retry anterior se active.
if isinstance(payload, str) and payload.startswith('{"statusCode"') and "ERR_" in payload:
raise ValueError(f"Error del navegador en la nube: {payload[:200]}")
return payload
@tool
@_retry
def google_search(q: str, hl: str = "es", gl: str = "us", num: int = 5) -> str:
"""Buscar en Google y devolver los principales resultados orgánicos como JSON."""
return _check(str(_search.invoke({"q": q, "hl": hl, "gl": gl, "num": num})))
@herramienta
@_reintentar
def renderizar_pagina(url: str) -> str:
"""Renderiza una URL con el navegador en la nube de Scrapeless y devuelve markdown limpio."""
return _comprobar(_raspado.invoke({"url": url, "response_type": "markdown"}))
@herramienta
@_reintentar
def rastrear_sitio(url: str, limite: int = 10) -> str:
"""Rastrear un sitio hasta un conteo de páginas limitado, devolviendo markdown para cada página."""
return _comprobar(str(_raspado.invoke({"url": url, "limit": limite})))
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
mensaje_del_sistema = (
"Eres un agente de investigación que construye conjuntos de datos de productos tipados. "
"Dada una consulta de categoría, tú: "
"(1) llamas a google_search para las principales URL orgánicas, "
"(2) llamas a renderizar_pagina con cada URL prometedora, "
"(3) extraes un registro de producto por página utilizando el esquema, "
"(4) devuelves un array JSON de registros. "
"Fija gl='us' y hl='en' a menos que el usuario pida lo contrario. "
"Si una página carece de un precio, omite la fila del array final."
)
agente = crear_agente(llm,
[google_search, renderizar_pagina, rastrear_sitio],
mensaje_del_sistema=mensaje_del_sistema)
for chunk in agente.stream(
{"messages": [("humano",
"Encuentra las 3 mejores máquinas de espresso portátiles por menos de $150 "
"y devuelve nombre, precio, calificación, conteo_de_revisiones, características_clave, url.")]},
modo_de_flujo="valores",
):
chunk["messages"][-1].imprimir_bonito()
Paso 6 — Dureza para producción
Un script de investigación que funciona en tres URL en un cuaderno no sobrevive a treinta mil. Cuatro patrones de endurecimiento convierten el pipeline anterior en algo que un programador puede ejecutar sin supervisión.
Concurrencia limitada
python
# renderizado_concurrente.py
import asyncio
from langchain_scrapeless import HerramientaUniversalDeRaspadoScrapeless
raspado = HerramientaUniversalDeRaspadoScrapeless()
SEM = asyncio.Semaphore(3) # limita a 3 renderizados concurrentes por host
async def renderizar(url: str) -> str:
async with SEM:
return await raspado.ainvoke({"url": url, "response_type": "markdown"})
async def renderizar_todas(urls: list[str]) -> list[str]:
return await asyncio.gather(*(renderizar(u) for u in urls))
Tres renderizados concurrentes por host es el punto óptimo: lo suficientemente alto como para amortiguar el costo de calentamiento por sesión, lo suficientemente bajo como para mantenerse por debajo de los límites de tasa por IP de la mayoría de los sitios. Limita a nivel de host, no globalmente; diez hosts diferentes con tres trabajadores cada uno está bien, diez trabajadores todos golpeando el mismo minorista no lo están.
Reintentar en errores transitorios
python
# reintentar.py
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from langchain_scrapeless import HerramientaUniversalDeRaspadoScrapeless
raspado = HerramientaUniversalDeRaspadoScrapeless()
def _levantar_en_error_interno(payload: str) -> str:
# Scrapeless a veces devuelve HTTP 200 con un cuerpo JSON describiendo un
# error interno del lado del navegador (reinicio de túnel, ERR_CONNECTION_RESET, …).
# La herramienta no se activará por eso; presenta como ValueError para que se active el reintento.
if payload.startswith('{"statusCode"') and "ERR_" in payload:
raise ValueError(f"Error en el navegador en la nube: {payload[:200]}")
return payload
@retry(
stop=stop_after_attempt(4),
wait=wait_exponential(multiplier=1, min=2, max=20),
retry=retry_if_exception_type((ValueError, TimeoutError)),
)
def renderizar_con_reintento(url: str) -> str:
return _levantar_en_error_interno(
raspado.invoke({"url": url, "response_type": "markdown"}))
La sesión del navegador de scraping ocasionalmente falla de dos formas distintas: un 400/503 a nivel HTTP (el envoltorio langchain-scrapeless lo eleva como ValueError) y un HTTP 200 con un cuerpo JSON que describe un error del lado del navegador como ERR_TUNNEL_CONNECTION_FAILED (el envoltorio no eleva — el error JSON regresa como una cadena). El guardián _raise_on_embedded_error arriba captura la segunda forma y la convierte en un ValueError para que se aplique la misma política de reintentos. Con ambas formas cubiertas, la retroalimentación exponencial con cuatro intentos captura el percentil alto-90 sin enterrar fallos genuinos (bloqueos anti-bot, 404) bajo reintentos. Para llamadas impulsadas por agentes, utiliza el apilamiento de decoradores @tool + @retry del Paso 5 — las funciones de envoltura allí deben aplicar el mismo chequeo _raise_on_embedded_error antes de regresar.
Observabilidad con LangSmith
bash
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY="tu_clave_langsmith"
export LANGCHAIN_PROJECT="agente-investigación-sin-scrapear"
Con esas tres variables de entorno establecidas, cada llamada de herramienta, cada llamada de LLM y cada fallo de analizador aparece en LangSmith con temporización, costo y el prompt exacto que el modelo vio. Para una ejecución en producción, este es el cambio de mayor apalancamiento — convierte “el agente hizo algo raro” en un rastro clickeable.
Persistir en un almacén de vectores
python
# embed.py
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
docs = [Document(page_content=p.model_dump_json(), metadata={"url": str(p.url)})
for p in products]
vs = Chroma.from_documents(docs, OpenAIEmbeddings(), persist_directory=".chroma")
El almacén de vectores es la cuarta etapa opcional de la canalización. Vale la pena cuando el agente es un servicio RAG de larga duración que responde preguntas posteriores sobre el conjunto de datos; es excesivo para un script de investigación de una sola vez. Elige el almacén en función de la preferencia de operaciones: langchain_chroma para archivos locales, langchain_postgres para un Postgres administrado + pgvector, langchain_pinecone para una base de datos vectorial alojada.
Lo que obtienes a cambio
json
[
{
"name": "Wacaco Nanopresso",
"price": 79.95,
"rating": 4.7,
"review_count": 12483,
"key_features": [
"Operación manual con bomba de mano",
"Hasta 18 bares de presión de extracción",
"Compatible con café molido o cápsulas NS mediante adaptador"
],
"url": "https://ejemplo.com/p/wacaco-nanopresso"
},
{
"name": "Flair NEO Flex",
"price": 119.00,
"rating": 4.5,
"review_count": 2104,
"key_features": [
"Accionado por palanca, no requiere electricidad",
"Presión de calidad de espresso con filtro de café sin fondo",
"Desmontable para viajar"
],
"url": "https://ejemplo.com/p/flair-neo-flex"
},
{
"name": "Outin Nano",
"price": 129.99,
"rating": 4.6,
"review_count": 5871,
"key_features": [
"Elemento calefactor incorporado",
"Ciclo de autolimpieza",
"Carga USB-C, calentamiento en ~3 minutos"
],
"url": "https://ejemplo.com/p/outin-nano"
}
]
// El esquema refleja exactamente lo que emite el analizador del Paso 4. Los valores de los campos son muestras ilustrativas.
Algunas observaciones honestas sobre qué esperar cuando esto se ejecute en la web en vivo:
- El tiempo de hidratación varía según el sitio. La herramienta de scraping universal espera
domcontentloadedpor defecto; para SPAs que hidratan precios a través de una segunda XHR, el markdown puede llegar antes de que el precio se renderice. Re-renderiza una vez con un breve retraso o recurre aresponse_type="html"y un selector personalizado si el campo es consistentemente nulo. - Los campos opcionales permanecen opcionales. Algunas páginas de productos omiten clasificaciones o recuentos de reseñas explícitos, especialmente en sitios directos al consumidor. Trata los valores
nullcomo informativos en lugar de como un modo de fallo y filtra a continuación. - La extracción de LLM domina la latencia. De extremo a extremo, la canalización tarda aproximadamente 1-3 segundos para la llamada de búsqueda, 2-4 segundos por renderizado de página y 3-5 segundos por extracción de LLM. La concurrencia en las etapas de renderizado y extracción es la palanca más grande.
- Los intersticiales anti-bot se presentan como
ValueError. Cuando un sitio carga por adelantado un desafío de Cloudflare o Akamai que el navegador en la nube no puede completar de manera transparente, el envoltoriolangchain-scrapelesselevaValueErroren lugar de devolver silenciosamente una página de marcador de posición. El decorador de reintento captura los casos transitorios; los casos persistentes se manejan mejor ampliando la huella digital o utilizando una región de proxy diferente. - La etapa del almacén de vectores es opcional. Para una canalización de investigación que devuelve registros tipados a un consumidor descendente, omítela por completo. Agrégala cuando el mismo conjunto de datos responda múltiples preguntas descendentes a lo largo del tiempo.
Preguntas frecuentes
¿Necesito un proxy residencial?
Sí, para cualquier sitio con una protección anti-bot significativa, que son la mayoría de los minoristas, mercados y puntos finales de SERP. ScrapelessUniversalScrapingTool y las herramientas de deep-SERP recorren por defecto el grupo de proxies residenciales de Scrapeless; el parámetro gl en la herramienta de búsqueda fija el país de salida.
¿Qué pasa con errores transitorios como 400 o 503?
Las herramientas langchain-scrapeless presentan errores transitorios de API como ValueError (el subyacente ScrapelessError se envuelve antes de volver a lanzar). Para llamadas directas, usa el decorador tenacity del Paso 6 con retry_if_exception_type=(ValueError, TimeoutError). Para llamadas impulsadas por agentes dentro de create_agent, envuelve cada herramienta con la pila de decoradores @tool + @retry del Paso 5; eso produce un verdadero StructuredTool que el agente acepta y aplica la política de reintentos en cada llamada a la herramienta. Sin uno de estos, un único 400 transitorio hace que toda la ejecución del agente se caiga.
Un sitio devuelve Acceso Denegado. ¿Qué hago ahora?
Primero, vuelve a intentar con el decorador del Paso 6. Si la página bloquea de manera persistente, amplía la sesión cambiando gl a un país diferente o agrega un breve await asyncio.sleep(...) entre intentos para permitir que el estado de la sesión se enfríe. Para sitios con bloqueos consistentes a nivel de IP, contacta al soporte de Scrapeless para confirmar que el bloqueo está a nivel de plataforma en lugar de a nivel de cuenta.
Los selectores siguen fallando. ¿Cómo sobrevivo a la rotación del DOM?
Usa response_type="markdown" en ScrapelessUniversalScrapingTool en lugar de analizar HTML con selectores CSS. Markdown colapsa el chrome de navegación y la mayor parte de la deriva de diseño, por lo que el extractor LLM en el Paso 4 ve una representación estable del contenido incluso cuando el DOM subyacente cambia.
¿Cuántos trabajadores concurrentes por host?
Tres es el límite documentado para ejecuciones estables. Limítate a nivel de host (asyncio.Semaphore(3) en el Paso 6); los trabajadores en diferentes hosts pueden ejecutarse de manera independiente.
¿Puedo usar esto sin LangGraph?
Sí. ScrapelessUniversalScrapingTool().invoke({...}) es un llamado simple: puedes llamarlo desde cualquier script de Python, ruta de FastAPI o tarea de Celery. LangGraph agrega el bucle agente por encima, pero las herramientas en sí mismas son independientes del marco.
¿Puedo intercambiar OpenAI por Claude, Gemini o un modelo local?
Sí. Reemplaza ChatOpenAI(model="gpt-4o-mini") por ChatAnthropic(model="claude-sonnet-4-6"), ChatGoogleGenerativeAI(model="gemini-2.5-pro"), ChatOllama(model="llama3.1") o cualquier otro modelo de chat de LangChain. La lista de tools, el prompt y el analizador no cambian.
¿Cómo añado memoria de múltiples turnos?
Pasa un comprobador MemorySaver a create_agent(llm, tools, checkpointer=MemorySaver()) y proporciona un thread_id en cada invocación. LangGraph persiste el estado de la conversación a través de los turnos, por lo que el agente puede referirse a búsquedas anteriores sin tener que volver a ejecutarlas.
¿Dónde puedo ver las trazas de las solicitudes?
Establece LANGCHAIN_TRACING_V2=true, LANGCHAIN_API_KEY y LANGCHAIN_PROJECT (Paso 6). Cada llamada a la herramienta, llamada LLM y ejecución del analizador aparece en LangSmith con tiempos, costos y el prompt exacto: el cambio de observabilidad de mayor aprovechamiento para un despliegue en producción.
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.



