En este artículo, aprenderá cómo crear agentes de IA que puedan navegar e interactuar con sitios web reales utilizando Playwright, el uso del navegador y LangGraph.
Los temas que cubriremos incluyen:
Por qué Playwright es la base adecuada para la automatización del navegador en 2026 y en qué se diferencia de Selenium. Cómo extraer páginas dinámicas renderizadas en JavaScript y completar formularios de varios pasos de manera confiable. Cómo conectar las acciones del navegador a LangGraph y a los agentes de uso del navegador, manejar la detección anti-bot, administrar la espera y la persistencia de la sesión e implementar el resultado en Docker.
Creación de agentes de IA que utilizan navegador en Python
Introducción
La mayoría de los tutoriales de agentes de IA comienzan con una API. Le muestran cómo llamar a OpenWeather, acceder al punto final de Stripe y extraer datos de GitHub. Ese es un buen punto de partida hasta que intentas construir algo real y te das cuenta de que la tarea que realmente necesitas realizar no tiene una API.
Piense en lo que los humanos hacen con los navegadores todos los días: presentar formularios gubernamentales, leer los precios de la competencia, extraer investigaciones de sitios que protegen sus datos detrás del procesamiento de JavaScript, iniciar sesión en portales que nunca han oído hablar de OAuth. Hay aproximadamente 1.100 millones de sitios web en Internet. Una fracción cada vez más pequeña de ellos tiene API públicas. El resto sólo habla navegador.
Un agente que está limitado a llamadas API maneja quizás el 5% de las tareas que realiza un trabajador humano diariamente. Dale a ese agente un navegador y la cobertura cubrirá todo. Ésa es la brecha que cierra este artículo.
El mercado mundial de agentes de IA asciende a 10.910 millones de dólares en 2026 y se prevé que alcance los 50.310 millones de dólares en 2030, con los agentes con capacidad de navegador en el centro de ese crecimiento. El 27,7% de las empresas ya utilizan navegadores agentes en producción, frente a prácticamente ninguna dos años antes. Las herramientas han madurado rápidamente y los patrones están lo suficientemente establecidos para enseñar correctamente.
Al final de este artículo, tendrá un agente de navegador funcional que navega por sitios web reales, completa formularios, extrae datos estructurados y se conecta a un LLM que decide qué hacer a continuación, todo en Python.
Por qué dramaturgo y no selenio
Si creó la automatización del navegador hace cinco años, la creó con Selenium. El selenio todavía está ampliamente implementado, todavía funciona y no irá a ninguna parte. Pero para cualquier proyecto nuevo en 2026, Playwright es el predeterminado. Las razones son prácticas, no teóricas.
Selenium se comunica con el navegador enviando solicitudes HTTP individuales a un WebDriver. Cada acción, hacer clic, escribir, desplazarse, es una solicitud separada. Playwright utiliza una conexión WebSocket persistente durante toda la sesión. Los comandos fluyen a través de ese canal sin costo de ida y vuelta por acción. Los puntos de referencia independientes muestran consistentemente que Playwright corre entre un 30% y un 50% más rápido que Selenium en el nivel del conjunto de pruebas y con un promedio de ~290 ms por acción frente a los ~536 ms de Selenium. Para un agente de navegador que podría ejecutar cientos de acciones, esa brecha se agrava.
Playwright también incluye sus propios archivos binarios de navegador. Cuando lo instalas, obtienes versiones preconfiguradas de Chromium, Firefox y WebKit que están garantizadas para funcionar con tu versión de Playwright. No hay discrepancias en las versiones del controlador ni tuberías de CI rotas porque alguien actualizó Chrome. Tiene una espera automática incorporada antes de hacer clic en un elemento; verifica que el elemento esté visible, habilitado y no animado. No es necesario escribir time.sleep(2) y esperar lo mejor.
Específicamente para los agentes de IA, Playwright activa eventos reales de mouse y teclado que reflejan cómo los humanos interactúan con los navegadores. Los sitios diseñados para detectar la automatización buscan clics DOM sintéticos. El modelo de interacción del dramaturgo es más difícil de distinguir del aporte humano genuino.
También está la biblioteca de uso del navegador, que se encuentra un nivel más arriba. El uso del navegador es una biblioteca de Python que le brinda a un LLM un navegador que funciona. En el fondo, utiliza Playwright para controlar el navegador, pero el LLM lee el estado de la página y decide en qué hacer clic, escribir y extraer, sin necesidad de selectores de CSS. Le asignas una tarea en inglés sencillo y él se da cuenta del resto. En este artículo cubriremos tanto el uso de Playwright como el del navegador, porque satisfacen diferentes necesidades: Playwright cuando desea un control preciso y predecible; uso del navegador cuando desee que el agente maneje las decisiones de navegación de forma autónoma.
Configurar el entorno
Necesita Python 3.10 o superior, una clave API de OpenAI y unos cinco minutos.
Paso 1: crear un entorno virtual
python -m venv browser_agent_env
# macOS / Linux
source browser_agent_env/bin/activate
# Windows
browser_agent_envScriptsactivate
python – m venv browser_agent _ env
# macOS / Linux
source browser_agent_env / bin / activate
# Windows
browser_agent_env Scripts activate
Paso 2: instalar dependencias
pip install playwright
browser-use
langchain
langchain-openai
langgraph
langchain-community
python-dotenv
pip install playwright
browser – use
langchain
langchain – openai
langgraph
langchain – community
python – dotenv
Paso 3: instale los archivos binarios del navegador
Este es el paso que la mayoría de la gente pasa por alto. Playwright necesita descargar Chromium, Firefox y WebKit por separado del paquete Python. Ejecute esto una vez después de la instalación:
playwright install chromium
playwright install chromium
Si desea los tres motores de navegador: instale dramaturgo. Chromium por sí solo es suficiente para la mayoría del trabajo del agente y es más pequeño para descargar.
Paso 4: almacene su clave API
Cree un archivo .env en el directorio de su proyecto:
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_API_KEY = your_openai_api_key_here
Agregue .env a su .gitignore inmediatamente. No confirme claves API.
Paso 5: Verifica que todo funcione
Aquí hay una primera secuencia de comandos que navega a una URL, lee el encabezado y guarda una captura de pantalla. Utilice example.com, un dominio de prueba disponible públicamente mantenido por la IANA que no lo bloqueará.
Cómo ejecutar: guarde como first_run.py y ejecute python first_run.py
# first_run.py
# Navigate to a URL, take a screenshot, and extract the page title.
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python first_run.py
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
# Launch Chromium in headless mode (no visible browser window).
# Set headless=False if you want to watch it run during development.
browser = await p.chromium.launch(headless=True)
# A browser context is like a fresh browser profile.
# It isolates cookies, storage, and cache from other contexts.
context = await browser.new_context(
viewport={“width”: 1280, “height”: 720},
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 = await context.new_page()
# Navigate to the URL and wait until the network is idle.
# “networkidle” means no open network connections for 500ms.
# For faster pages, “domcontentloaded” is sufficient.
await page.goto(“https://example.com”, wait_until=”networkidle”)
# Extract the page title
title = await page.title()
print(f”Page title: {title}”)
# Extract the text content of the h1 heading
h1 = await page.text_content(“h1″)
print(f”H1 heading: {h1}”)
# Take a full-page screenshot and save it to disk
await page.screenshot(path=”screenshot.png”, full_page=True)
print(“Screenshot saved to screenshot.png”)
await browser.close()
asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# first_run.py
# Navigate to a URL, take a screenshot, and extract the page title.
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python first_run.py
import asyncio
from playwright . async_api import async_playwright
async def main ( ) :
async with async_playwright ( ) as p :
# Launch Chromium in headless mode (no visible browser window).
# Set headless=False if you want to watch it run during development.
browser = await p . chromium . launch ( headless = True )
# A browser context is like a fresh browser profile.
# It isolates cookies, storage, and cache from other contexts.
context = await browser . new_context (
viewport = { “width” : 1280 , “height” : 720 } ,
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 = await context . new_page ( )
# Navigate to the URL and wait until the network is idle.
# “networkidle” means no open network connections for 500ms.
# For faster pages, “domcontentloaded” is sufficient.
await page . goto ( “https://example.com” , wait_until = “networkidle” )
# Extract the page title
title = await page . title ( )
print ( f “Page title: {title}” )
# Extract the text content of the h1 heading
h1 = await page . text_content ( “h1” )
print ( f “H1 heading: {h1}” )
# Take a full-page screenshot and save it to disk
await page . screenshot ( path = “screenshot.png” , full_page = True )
imprimir ( "Captura de pantalla guardada en captura de pantalla.png" )
Espera al navegador . cerca ( )
asincio . ejecutar ( principal ( ) )
Qué hace esto: async_playwright() es el punto de entrada para toda la sesión de Playwright. Browser_context equivale a abrir una nueva ventana de incógnito; las cookies, el almacenamiento local y el caché están aislados de todo lo demás. wait_until=”networkidle” le dice a Playwright que espere hasta que la página haya finalizado toda su actividad de red antes de que su código continúe, que es la estrategia de espera más segura para páginas dinámicas.
Si esto se ejecuta y guarda una captura de pantalla, su entorno está funcionando correctamente.
Navegación web y scraping
La razón por la que necesita Playwright en lugar de solicitudes + BeautifulSoup es la representación de JavaScript. Los sitios web modernos ofrecen un esqueleto de HTML y luego crean el contenido real dinámicamente después de que se carga la página: React, Vue, Angular, Next.js. Una solicitud HTTP simple recupera el esqueleto. Playwright ejecuta un navegador real, por lo que ve exactamente lo que ve un humano después de que se haya ejecutado todo JavaScript.
El siguiente objetivo es books.toscrape.com, un entorno de pruebas de scraping legal creado para la práctica. Pagina resultados, utiliza nombres de clases dinámicos para las calificaciones y refleja fielmente la estructura de las páginas reales de productos de comercio electrónico.
Cómo ejecutar: guarde como scrape_books.py y ejecute python scrape_books.py
# Scrape book titles, prices, and ratings from books.toscrape.com
# This is a legal scraping sandbox site built for practice.
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python scrape_books.py
import asyncio
import json
from playwright.async_api import async_playwright
async def scrape_books(max_pages: int = 3) -> list[dict]:
“””
Scrape book listings from books.toscrape.com across multiple pages.
Returns a list of dicts with title, price, rating, and page number.
“””
results =[]async con async_playwright() como p: browser = await p.chromium.launch(headless=True) context = await browser.new_context(viewport={"width": 1280, "height": 720}) page = await context.new_page() para page_num in range(1, max_pages + 1): url = f"https://books.toscrape.com/catalogue/page-{page_num}.html" print(f"Página de raspado {page_num}: {url}") await page.goto(url, wait_until="domcontentloaded") # Espere a que las tarjetas de producto estén visibles antes de extraer. # Esto es fundamental en páginas con mucho JavaScript donde el contenido se carga después del HTML. # timeout=10000 significa esperar hasta 10 segundos antes de generar un error. await page.wait_for_selector("article.product_pod", timeout=10000) # Obtener todas las tarjetas de libros en la página actual libros = await page.query_selector_all("article.product_pod") para libros en libros: # Extraer título del atributo de título de la etiqueta title_el = await book.query_selector("h3 a") title = await title_el.get_attribute("title") if title_el else "N/A" # Extraer el texto del precio price_el = await book.query_selector(".price_color") precio = await price_el.inner_text() if price_el else "N/A" # Extraer la calificación de estrellas del nombre de la clase CSS. # p.ej
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# scrape_books.py
# Extraiga títulos de libros, precios y calificaciones de books.toscrape.com
# Este es un sitio sandbox de scraping legal creado para la práctica.
# Requisitos previos: pip install dramaturgo && dramaturgo instala chromium
# Cómo ejecutar: python scrape_books.py
importar asincio
importar json
del dramaturgo . async_api importar async_playwright
async def scrape_books ( max_pages : entero = 3 ) -> lista [ dictar ] :
"" "
Raspe listados de libros de books.toscrape.com en varias páginas.
Devuelve una lista de dictados con título, precio, calificación y número de página.
" ""
resultados = [ ]
asíncrono con async_playwright ( ) como pag :
navegador = esperar pag . cromo . lanzamiento ( sin cabeza = Verdadero )
contexto = Espera al navegador . nuevo_contexto ( ventana gráfica = { "ancho" : 1280 , "altura" : 720 } )
página = esperar contexto . nueva_página ( )
para núm_página en rango ( 1 , páginas_max + 1 ) :
URL = f "https://books.toscrape.com/catalogue/page-{page_num}.html"
print ( f "Página raspada {page_num}: {url}" )
espera la página . ir a ( url , wait_until = "domcontentloaded" )
# Espere a que las tarjetas de producto sean visibles antes de extraerlas.
# Esto es fundamental en páginas con mucho JavaScript donde el contenido se carga después del HTML.
# timeout=10000 significa esperar hasta 10 segundos antes de generar un error.
espera la página . wait_for_selector ( "artículo.product_pod" , tiempo de espera = 10000 )
# Obtener todas las tarjetas de libros en la página actual
libros = espera la página . query_selector_all ( "artículo.product_pod" )
para registrarse libros :
título_el = espera libro . query_selector ( "h3 a" )
título = espera title_el . get_attribute ( "título" ) si título_el otro "N / A"
# Extraer texto del precio
precio_el = espera libro . query_selector ( ".precio_color" )
precio = espera precio_el . texto_interior ( ) si precio_el otro "N / A"
# Extraer la calificación de estrellas del nombre de la clase CSS.
calificación_el = espera libro . query_selector ( "p.calificación de estrellas" )
clase_clasificación = a la espera de rating_el . get_attribute ( "clase" ) si rating_el más ""
clasificación = clase_calificación . reemplazar ( "calificación de estrellas" , "" ) . banda ( )
resultados . agregar ( {
"título" : título ,
"precio" : precio ,
"calificación" : calificación ,
"página" : página _ número
} )
print ( f " Extraído {len(libros)} libros de la página {page_num}" )
Espera al navegador . cerca ( )
devolver resultados
asíncrono def principal ( ) :
libros = aguarde scrape_books ( max_pages = 2 )
print ( f "nTotal de libros raspados: {len(libros)}" )
print ( json . dumps ( libros [ : 3 ] , sangría = 2 ) )
asincio . ejecutar ( principal ( ) )
Qué hace esto: wait_for_selector() es la llamada clave aquí. En lugar de dormir durante un tiempo fijo y esperar que el contenido se haya cargado, observa el DOM y continúa en el momento en que aparece el elemento de destino, o genera un TimeoutError si no aparece dentro de la ventana de tiempo de espera. Ese es el comportamiento correcto: fallar rápida y explícitamente en lugar de extraer silenciosamente de una página vacía.
La extracción de calificaciones merece atención. La calificación de estrellas está codificada como una clase CSS (calificación de tres estrellas), no como un número. El código elimina la "calificación de estrellas" de la cadena de clase para obtener el valor del texto. Este es el tipo de cosas que sólo sabes inspeccionando el HTML real. Cuando entregas esta tarea a un LLM sin formato sin navegador, no tiene forma de saber cómo se ve la estructura de clases. Con Playwright, puedes inspeccionarlo directamente y extraerlo exactamente.
Finalización de formularios y flujos de varios pasos
Completar formularios es donde los agentes del navegador se ganan la vida y donde fallan la mayoría de los scripts de automatización. La razón es que los formularios web no son sólo entradas y botones. Activan eventos de enfoque, entrada, cambio y desenfoque en secuencia. La validación de JavaScript escucha esos eventos. Si inyecta un valor en un campo de entrada estableciendo directamente el valor en el DOM (como suelen hacer las herramientas de automatización más antiguas), los oyentes de validación nunca se activan y el formulario se rompe.
Los métodos fill() y click() de Playwright activan eventos reales del navegador en el orden correcto, por lo que funcionan en la validación de formularios que bloquearían los enfoques de nivel inferior.
El objetivo a continuación es the-internet.herokuapp.com/login, un sitio de prueba público mantenido específicamente para la práctica de la automatización. ¡Acepta Tomsmith/SuperSecretPassword! como credenciales válidas y devuelve mensajes claros de éxito/fracaso.
Cómo ejecutar: guarde como form_submit.py y ejecute python form_submit.py
# form_submit.py
# Complete and submit a multi-field login form on a public demo site.
# Target: https://the-internet.herokuapp.com/login (public test site)
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python form_submit.py
import asyncio
from playwright.async_api import async_playwright
async def login_and_verify(username: str, password: str) -> dict:
“””
Attempt to log in to a demo site and return whether it succeeded.
Handles: input filling, button clicking, and result verification.
“””
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
await page.goto(“https://the-internet.herokuapp.com/login”)
# Wait for the form to be visible before interacting.
# state=”visible” is the default but makes the intent explicit.
await page.wait_for_selector(“#username”, state=”visible”)
# fill() clears the field first, then types the value.
# It fires the focus, input, and change events in order.
await page.fill(“#username”, username)
await page.fill(“#password”, password)
# click() fires real mouse events — mousedown, mouseup, click.
# This triggers JavaScript listeners that a plain DOM click misses.
await page.click(“button[type=”submit”]”)
# Wait for the page to settle after form submission
await page.wait_for_load_state(“networkidle”)
# Check which result element appeared
success_el = await page.query_selector(“.flash.success”)
error_el = await page.query_selector(“.flash.error”)
if success_el:
message = await success_el.inner_text()
result = {“success”: True, “message”: message.strip()}
elif error_el:
message = await error_el.inner_text()
result = {“success”: False, “message”: message.strip()}
else:
result = {“success”: False, “message”: “Unknown result”}
await browser.close()
return result
async def main():
# Valid credentials for the demo site
result = await login_and_verify(“tomsmith”, “SuperSecretPassword!”)
print(f”Valid login: {result}”)
# Invalid credentials to verify error handling
result_fail = await login_and_verify(“wronguser”, “wrongpass”)
print(f”Invalid login: {result_fail}”)
asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# form_submit.py
# Complete and submit a multi-field login form on a public demo site.
# Target: https://the-internet.herokuapp.com/login (public test site)
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python form_submit.py
import asyncio
from playwright . async_api import async_playwright
async def login_and_verify ( username : str , password : str ) -> dict :
“” “
Attempt to log in to a demo site and return whether it succeeded.
Handles: input filling, button clicking, and result verification.
“ “”
async with async_playwright ( ) as p :
browser = await p . chromium . launch ( headless = True )
context = await browser . new_context ( )
page = await context . new_page ( )
await page . goto ( “https://the-internet.herokuapp.com/login” )
# Wait for the form to be visible before interacting.
# state="visible" es el valor predeterminado pero hace que la intención sea explícita.
espera la página . wait_for_selector ( "#nombredeusuario" , estado = "visible" )
# fill() primero borra el campo y luego escribe el valor.
# Activa los eventos de enfoque, entrada y cambio en orden.
espera la página . llenar ( "#nombre de usuario" , nombre de usuario )
espera la página . llenar ( "#contraseña" , contraseña )
# click() activa eventos reales del mouse: mousedown, mouseup, clic.
# Esto activa los oyentes de JavaScript que se pierden con un simple clic en DOM.
espera la página . haga clic en ( "botón[tipo="enviar"]" )
# Espere a que la página se estabilice después del envío del formulario.
espera la página . wait_for_load_state ( "red inactiva" )
# Comprobar qué elemento de resultado apareció
éxito_el = espera la página . query_selector ( ".flash.success" )
error_el = espera la página . query_selector ( ".flash.error" )
si éxito_el :
mensaje = aguarde el éxito_el . texto_interior ( )
resultado = { "éxito" : Verdadero , "mensaje" : mensaje . banda ( ) }
elif error_el :
mensaje = espera error_el . texto_interior ( )
resultado = { "éxito" : FALSO , "mensaje" : mensaje . banda ( ) }
demás :
resultado = { "éxito" : FALSO , "mensaje" : "Resultado desconocido" }
Espera al navegador . cerca ( )
devolver resultado
asíncrono def principal ( ) :
# Credenciales válidas para el sitio de demostración
resultado = espere login_and_verify ( "tomsmith" , "¡Contraseña súper secreta!" )
print ( f "Inicio de sesión válido: {resultado}" )
# Credenciales no válidas para verificar el manejo de errores
resultado_fallo = await login_and_verify ( "usuario incorrecto" , "paso equivocado" )
print ( f "Inicio de sesión no válido: {result_fail}" )
asincio . ejecutar ( principal ( ) )
Qué hace esto: El patrón aquí, fill() → click() → wait_for_load_state() → verificar el elemento de resultado, es la plantilla para casi cualquier interacción de formulario. El wait_for_load_state(“networkidle”) después del envío es importante: sin él, consulta el DOM antes de que la página se haya actualizado y obtiene el estado previo al envío, no el resultado.
Para formularios más complejos con carga de archivos, menús desplegables y casillas de verificación:
# File upload
await page.set_input_files(“#file-upload”, “/path/to/document.pdf”)
# Select dropdown by visible label text
await page.select_option(“#country-select”, label=”Nigeria”)
# Check a checkbox
await page.check(“#agree-terms”)
# Handle a modal dialog (confirm/alert)
page.on(“dialog”, lambda dialog: asyncio.ensure_future(dialog.accept()))
# File upload
espera la página . set_input_files ( "#carga-archivo" , "/ruta/al/documento.pdf" )
# Seleccionar menú desplegable por texto de etiqueta visible
espera la página . select_option ( "#país-seleccionar" , etiqueta = "Nigeria" )
# Marque una casilla de verificación
espera la página . comprobar ( "#acuerdo-términos" )
# Manejar un diálogo modal (confirmar/alertar)
página . en ( "diálogo" , diálogo lambda : asincio . asegurar_futuro ( diálogo . aceptar ( ) ) )
Orquestación de herramientas con LangChain y LangGraph
Los guiones de Raw Playwright son potentes pero fijos. Hacen exactamente lo que codificaste, nada más. En el momento en que una página cambia su estructura, o la tarea requiere una decisión que el guión no anticipó, se rompe.
Conectar a Playwright con un LLM cambia esto. Las acciones del navegador se convierten en herramientas que el agente puede utilizar cuando decide que son necesarias. El agente lee la tarea, razona qué hacer, llama a una herramienta, lee el resultado y decide qué hacer a continuación. Ese bucle maneja variaciones que un guión fijo no puede.
Este es el puente entre el "script de automatización del navegador" y el "agente de IA".
Cómo ejecutar: guarde como agent_tools.py, asegúrese de que OPENAI_API_KEY esté en su .env, luego ejecute python agent_tools.py
# agent_tools.py
# LangGraph agent with three browser tools: navigate_and_extract, fill_and_submit_form, take_screenshot
# Prerequisites: pip install playwright langchain langchain-openai langgraph python-dotenv
# playwright install chromium
# How to run: python agent_tools.py
import asyncio
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
from playwright.async_api import async_playwright
load_dotenv()
# ── SHARED BROWSER STATE ──────────────────────────────────────────────────────
# We keep a single browser instance alive for the agent’s lifetime.
# Creating and destroying a browser on every tool call is slow and wasteful.
_browser = None
_page = None
_playwright = None
async def get_page():
“””Return the shared page, launching the browser if needed.”””
global _browser, _page, _playwright
if _browser is None:
_playwright = await async_playwright().start()
_browser = await _playwright.chromium.launch(headless=True)
context = await _browser.new_context(viewport={“width”: 1280, “height”: 720})
_page = await context.new_page()
return _page
async def close_browser():
“””Clean up browser resources when the agent session ends.”””
global _browser, _page, _playwright
if _browser:
await _browser.close()
await _playwright.stop()
_browser = None
_page = None
_playwright = None
# ── BROWSER TOOLS ─────────────────────────────────────────────────────────────
# Note: these are async tools (async def). LangChain’s @tool decorator supports
# async functions directly, and the agent must be invoked with ainvoke() so that
# tool calls run on the same event loop instead of trying to start a second one.
@tool
async def navigate_and_extract(url: str) -> str:
“””
Navigate to a URL and return the visible text content of the page.
Use this to visit websites and read their content.
Input: a full URL string including https:// (e.g., ‘https://example.com’).
“””
page = await get_page()
await page.goto(url, wait_until=”domcontentloaded”, timeout=15000)
await page.wait_for_load_state(“networkidle”)
content = await page.inner_text(“body”)
# Truncate to avoid flooding the LLM context window
return content[:3000] if len(content) > 3000 else content
@tool
async def fill_and_submit_form(selector_value_pairs: str) -> str:
“””
Fill form fields and submit a form on the currently loaded page.
Input: a comma-separated string of ‘selector:value’ pairs ending with ‘submit:button_selector’.
Example: ‘#email:user@example.com,#password:secret,submit:button[type=submit]’
“””
page = await get_page()
try:
pairs = selector_value_pairs.split(“,”)
submit_selector = None
for pair in pairs:
key, val = pair.split(“:”, 1)
key = key.strip()
val = val.strip()
if key == “submit”:
submit_selector = val
else:
await page.fill(key, val)
if submit_selector:
await page.click(submit_selector)
await page.wait_for_load_state(“networkidle”)
return f”Form submitted. Current URL: {page.url}”
except Exception as e:
return f”Form interaction failed: {str(e)}”
@tool
async def take_screenshot(filename: str) -> str:
“””
Take a screenshot of the current browser page and save it to a file.
Use this to visually verify the current state of the page.
Input: filename string (e.g., ‘result.png’).
“””
page = await get_page()
await page.screenshot(path=filename, full_page=False)
return f”Screenshot saved to {filename}”
# ── AGENT SETUP ───────────────────────────────────────────────────────────────
llm = ChatOpenAI(
model=”gpt-4o”,
temperature=0,
api_key=os.getenv(“OPENAI_API_KEY”)
)
tools = [navigate_and_extract, fill_and_submit_form, take_screenshot]
# create_react_agent wires together the LLM, the tools, and the ReAct reasoning loop.
# The agent decides which tool to call, calls it, reads the result, and continues.
agent = create_react_agent(llm, tools)
# ── DEMO ──────────────────────────────────────────────────────────────────────
async def main():
result = await agent.ainvoke({
“messages”: [HumanMessage(
content=(
“Go to https://example.com, read the page content, “
“then take a screenshot called example.png”
)
)]
})
print(result[“messages”][-1].content)
await close_browser()
asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# agent_tools.py
# LangGraph agent with three browser tools: navigate_and_extract, fill_and_submit_form, take_screenshot
# Prerequisites: pip install playwright langchain langchain-openai langgraph python-dotenv
# playwright install chromium
# How to run: python agent_tools.py
import asyncio
importar sistema operativo
desde dotenv importar load_dotenv
desde langchain_openai importar ChatOpenAI
de cadena larga . herramienta de importación de herramientas
de langchain_core . importar mensajes HumanMessage
de langgraph . importación prediseñada create_react_agent
del dramaturgo . async_api importar async_playwright
cargar_dotenv ( )
# ── ESTADO DEL NAVEGADOR COMPARTIDO ─────────────────────────── ───────────────────────────
# Mantenemos activa una única instancia del navegador durante toda la vida del agente.
# Crear y destruir un navegador en cada llamada a una herramienta es lento y un desperdicio.
_navegador = Ninguno
_página = Ninguno
_dramaturgo = Ninguno
asíncrono def get_page ( ) :
"" "Regrese a la página compartida, iniciando el navegador si es necesario." ""
global _navegador , _página , _dramaturgo
si _el navegador es Ninguno :
_dramaturgo = espere async_playwright ( ) . comenzar ( )
_navegador = aguarda _dramaturgo . cromo . lanzamiento ( sin cabeza = Verdadero )
contexto = espere _navegador . nuevo_contexto ( ventana gráfica = { "ancho" : 1280 , "altura" : 720 } )
_página = esperar contexto . nueva_página ( )
devolver _página
asíncrono def close_browser ( ) :
"" "Limpiar los recursos del navegador cuando finalice la sesión del agente." ""
global _navegador , _página , _dramaturgo
si _navegador :
espere _navegador . cerca ( )
aguarda _dramaturgo . detener ( )
_navegador = Ninguno
_página = Ninguno
_dramaturgo = Ninguno
# ── HERRAMIENTAS DEL NAVEGADOR ────────────────────────────── ───────────────────────────────
# Nota: estas son herramientas asíncronas (async def). El decorador @tool de LangChain admite
# asíncrono funciona directamente y el agente debe invocarse con ainvoke() para que
# llamadas a herramientas se ejecutan en el mismo bucle de eventos en lugar de intentar iniciar uno segundo.
@ herramienta
async def navegar_and_extract ( url : cadena ) -> cadena :
"" "
Navegue a una URL y devuelva el contenido de texto visible de la página.
Úselo para visitar sitios web y leer su contenido.
Entrada: una cadena de URL completa que incluye https:// (por ejemplo, 'https://example.com').
" ""
página = espera get_page ( )
espera la página . ir a ( url , wait_until = "domcontentloaded" , tiempo de espera = 15000 )
espera la página . wait_for_load_state ( "red inactiva" )
contenido = espera la página . texto_interior ( "cuerpo" )
# Truncar para evitar inundar la ventana de contexto de LLM
devolver contenido [ : 3000 ] si len ( contenido ) > 3000 demás contenido
@ herramienta
async def fill_and_submit_form ( selector_valor_pares : cadena ) -> cadena :
"" "
Complete los campos del formulario y envíe un formulario en la página actualmente cargada.
Entrada: una cadena separada por comas de pares 'selector:valor' que terminan en 'enviar:botón_selector'.
Ejemplo: '#correo electrónico:usuario@ejemplo.com,#contraseña:secreto,enviar:botón[tipo=enviar]'
" ""
página = espera get_page ( )
intentar :
pares = pares_valor_selector . dividir ( "," )
enviar_selector = Ninguno
para par en pares :
llave , vale = par . dividir ( ":" , 1 )
llave = llave . banda ( )
vale = vale . banda ( )
si llave == "entregar" :
enviar_selector = vale
demás :
espera la página . llenar ( tecla , valor )
si enviar_selector :
espera la página . haga clic en ( enviar_selector )
espera la página . wait_for_load_state ( "red inactiva" )
devolver f "Formulario enviado. URL actual: {page.url}"
excepto excepción como mi :
devolver f "Error en la interacción del formulario: {str(e)}"
@ herramienta
async def tomar_captura de pantalla ( nombre de archivo : cadena ) -> cadena :
"" "
Tome una captura de pantalla de la página actual del navegador y guárdela en un archivo.
Utilícelo para verificar visualmente el estado actual de la página.
Entrada: cadena de nombre de archivo (por ejemplo, 'resultado.png').
" ""
página = espera get_page ( )
espera la página . captura de pantalla ( ruta = nombre de archivo , página_completa = Falso )
devolver f "Captura de pantalla guardada en {nombre de archivo}"
# ── CONFIGURACIÓN DEL AGENTE ─────────────────────────────── ────────────────────────────────
llm = ChatOpenAI (
modelo = "gpt-4o" ,
temperatura = 0 ,
api_key = sistema operativo . getenv ( "OPENAI_API_KEY" )
)
herramientas = [ navegar_y_extraer , llenar_y_enviar_formulario , tomar_captura de pantalla ]
# create_react_agent conecta el LLM, las herramientas y el ciclo de razonamiento de ReAct.
# El agente decide a qué herramienta llamar, la llama, lee el resultado y continúa.
agente = create_react_agent ( llm , herramientas )
# ── DEMOSTRACIÓN ─────────────────────────────────── ───────────────────────────────────
asíncrono def principal ( ) :
resultado = A la espera del agente . invocar ( {
"mensajes" : [ Mensaje Humano (
contenido = (
"Vaya a https://example.com, lea el contenido de la página",
"luego toma una captura de pantalla llamada ejemplo.png"
)
) ]
} )
imprimir ( resultado [ " mensajes" ] [ -1 ] . contenido )
aguarde close_browser ( )
asincio . ejecutar ( principal ( ) )
Qué hace esto: Las tres funciones @tool-decorated se registran con el agente. Cada cadena de documentación es lo que lee el LLM para comprender qué hace la herramienta y cuándo usarla. Escríbalos como descripciones de trabajo, no como comentarios de código. Los globales _browser y _page compartidos significan que el navegador permanece abierto en múltiples llamadas a herramientas, lo cual es esencial para tareas que abarcan varias páginas en la misma sesión. Debido a que las herramientas se definen con async def, el agente se invoca con ainvoke() en lugar de invoke(), por lo que las llamadas a la herramienta se ejecutan en el mismo bucle de eventos que main() ya está usando.
Un diagrama de flujo vertical que muestra cómo fluye una solicitud de tarea a través del agente (haga clic para ampliar)
Imagen del editor
La decisión de diseño clave en este fragmento es la instancia del navegador compartido. Si cada llamada de herramienta iniciara y cerrara su propio navegador, perdería todo el estado de la sesión entre llamadas, como las cookies, el historial de navegación y cualquier estado de formulario que el agente ya haya creado. Mantener el navegador activo durante la sesión completa del agente preserva ese contexto.
Uso del navegador para tareas de agentes de alto nivel
Raw Playwright con funciones @tool te brinda un control preciso. La desventaja es que todavía estás escribiendo selectores, todavía pensando en la estructura de la página y aún manejando cada caso extremo manualmente. Si el sitio cambia su HTML, sus selectores se rompen.
El uso del navegador adopta un enfoque diferente. En lugar de escribir selectores, le asigna al agente una tarea en inglés sencillo. El uso del navegador utiliza Playwright bajo el capó, pero el LLM lee el estado actual de la página en cada paso y decide qué hacer a continuación: en qué elemento hacer clic, qué escribir y cuándo se completa la tarea. La estructura de la página no está codificada en su código. El agente lo descubre en tiempo de ejecución.
uso del navegador es una biblioteca de Python que le brinda a un LLM un navegador que funciona. El LLM lee cada página y decide en qué hacer clic, escribir y extraer. Esto lo hace resistente a los cambios del sitio que romperían un script basado en selectores.
Cuándo utilizar el uso del navegador en Playwright sin formato:
Si la tarea es exploratoria y la estructura de la página es impredecible, utilice el navegador. Si está ejecutando un flujo de trabajo fijo y repetible donde cada selector es conocido y estable, Playwright sin formato es más confiable y más económico por ejecución. Un agente que utiliza el navegador realiza varias llamadas de LLM por paso de la tarea; una ejecución de Dramaturgo con guión no genera ninguno.
Cómo ejecutar: guarde como browser_use_agent.py, asegúrese de que OPENAI_API_KEY esté en su .env, luego ejecute python browser_use_agent.py
# browser_use_agent.py
# A browser-use agent that accepts a natural language task and completes it
# without any CSS selectors or hardcoded page structure.
# Prerequisites: pip install browser-use playwright python-dotenv
# playwright install chromium
# How to run: python browser_use_agent.py
import asyncio
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from browser_use import Agent
load_dotenv()
async def run_browser_task(task: str) -> str:
“””
Hand a natural language task to a browser-use agent.
The agent handles navigation, clicks, and extraction without selectors.
“””
# temperature=0 keeps decisions deterministic and reduces hallucinated actions
llm = ChatOpenAI(
model=”gpt-4o”,
temperature=0,
api_key=os.getenv(“OPENAI_API_KEY”)
)
# Agent wraps the browser, the LLM, and the task loop together.
# max_actions_per_step limits how many actions the agent takes before
# re-reading the page — prevents runaway loops on complex pages.
agent = Agent(
task=task,
llm=llm,
max_actions_per_step=5
)
# run() executes the full task loop:
# read page → decide action → take action → read updated page → repeat
result = await agent.run()
# final_result() returns the agent’s extracted content or conclusion
return result.final_result() or “Task completed with no extracted output.”
async def main():
task = (
“Go to https://books.toscrape.com and find the 3 most expensive books “
“on the first page. Return their titles and prices.”
)
print(f”Task: {task}n”)
output = await run_browser_task(task)
print(f”Result:n{output}”)
asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# browser_use_agent.py
# A browser-use agent that accepts a natural language task and completes it
# without any CSS selectors or hardcoded page structure.
# Prerequisites: pip install browser-use playwright python-dotenv
# playwright install chromium
# How to run: python browser_use_agent.py
import asyncio
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from browser_use import Agent
load_dotenv ( )
async def run_browser_task ( task : str ) -> str :
"" "
Entregue una tarea en lenguaje natural a un agente de uso del navegador.
El agente maneja la navegación, los clics y la extracción sin selectores.
" ""
# temperatura=0 mantiene las decisiones deterministas y reduce las acciones alucinadas
llm = ChatOpenAI (
modelo = "gpt-4o" ,
temperatura = 0 ,
api_key = sistema operativo . getenv ( "OPENAI_API_KEY" )
)
# El agente envuelve el navegador, el LLM y el bucle de tareas.
# max_actions_per_step limita cuántas acciones realiza el agente antes
# releer la página: evita bucles fuera de control en páginas complejas.
agente = Agente (
tarea = tarea ,
llm = llm ,
max_acciones_por_paso = 5
)
# run() ejecuta el ciclo completo de la tarea:
# leer página → decidir acción → tomar acción → leer página actualizada → repetir
resultado = A la espera del agente . correr ( )
# final_result() devuelve el contenido o la conclusión extraídos por el agente
devolver resultado . resultado_final ( ) o "Tarea completada sin resultados extraídos".
asíncrono def principal ( ) :
tarea = (
"Vaya a https://books.toscrape.com y encuentre los 3 libros más caros"
"en la primera página. Devuelve sus títulos y precios."
)
imprimir ( f "Tarea: {tarea}n" )
producción = espera run_browser_task ( tarea )
imprimir ( f "Resultado:n{salida}" )
asincio . ejecutar ( principal ( ) )
Qué hace esto: toda la tarea, navegar al sitio, leer la página, identificar los tres precios más altos y extraerlos, la maneja el agente sin un solo selector de CSS en su código. Si books.toscrape.com rediseña su visualización de precios mañana, el guión seguirá funcionando. Con un raspador basado en selector, se rompería silenciosamente.
Vale la pena explicar el parámetro max_actions_per_step=5. En cada paso, el agente lee la página y puede decidir realizar hasta cinco acciones (hacer clic, escribir, desplazarse, navegar) antes de volver a leer la página. Mantener este valor bajo obliga al agente a comprobar su trabajo con más frecuencia, lo que detecta los errores antes.
Manejando las partes duras
Tres cosas rompen la mayoría de los agentes de navegador en producción. Cada uno tiene una solución, pero ninguna es obvia hasta que ya estás quemado.
1. Detección antibots
Los sitios web que no quieren ser automatizados detectan la automatización de varias maneras, como verificando la propiedad navigator.webdriver (que Playwright establece en verdadero de forma predeterminada), buscando huellas digitales del navegador sin cabeza en el entorno JavaScript y analizando patrones de interacción que son demasiado rápidos o demasiado uniformes para ser humanos.
La mitigación más importante es eliminar la marca webdriver. Más allá de eso, una cadena de agente de usuario realista, un tamaño de ventana gráfica estándar y una ubicación y zona horaria realistas cubren la mayoría de los métodos de detección, excepto el análisis sofisticado de huellas dactilares.
# hard_parts.py — Part 1: Anti-bot stealth launch
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python hard_parts.py
import asyncio
import json
from pathlib import Path
from playwright.async_api import async_playwright
async def launch_stealth_browser(playwright):
“””
Launch a browser context that looks more like a real human session.
Covers: realistic viewport, user-agent, locale, timezone, webdriver flag.
Note: For serious anti-bot targets, consider a paid service like Browserbase.
“””
browser = await playwright.chromium.launch(
headless=True,
args=[
“–disable-blink-features=AutomationControlled”, # Hides webdriver detection
“–no-sandbox”,
“–disable-dev-shm-usage”,
]
)
context = await browser.new_context(
viewport={“width”: 1366, “height”: 768}, # Common desktop resolution
user_agent=(
“Mozilla/5.0 (Windows NT 10.0; Win64; x64) “
“AppleWebKit/537.36 (KHTML, like Gecko) “
“Chrome/124.0.0.0 Safari/537.36″
),
locale=”en-US”,
timezone_id=”America/New_York”,
java_script_enabled=True,
)
# Remove the ‘webdriver’ property that Playwright injects by default.
# Bot detection systems check for this in the browser’s JS environment.
await context.add_init_script(
“Object.defineProperty(navigator, ‘webdriver’, {get: () => undefined})”
)
return browser, context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# hard_parts.py — Part 1: Anti-bot stealth launch
# Prerequisites: pip install playwright && playwright install chromium
# How to run: python hard_parts.py
import asyncio
import json
from pathlib import Path
del dramaturgo . async_api importar async_playwright
async def launch_stealth_browser ( dramaturgo ) :
"" "
Inicie un contexto de navegador que se parezca más a una sesión humana real.
Cubre: ventana gráfica realista, agente de usuario, configuración regional, zona horaria, indicador de controlador web.
Nota: Para objetivos anti-bot serios, considere un servicio pago como Browserbase.
" ""
navegador = Espera al dramaturgo . cromo . lanzamiento (
sin cabeza = Verdadero ,
argumentos = [
"–disable-blink-features=AutomatizaciónControlada" , # Oculta la detección del controlador web
"–sin-zona de pruebas" ,
"–disable-dev-shm-usage" ,
]
)
contexto = Espera al navegador . nuevo_contexto (
ventana gráfica = { "ancho" : 1366 , "altura" : 768 } , # Resolución de escritorio común
agente_usuario = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, como Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
) ,
configuración regional = "en-US" ,
timezone_id = "América/Nueva_York" ,
java_script_enabled = Verdadero ,
)
# Elimina la propiedad 'webdriver' que Playwright inyecta de forma predeterminada.
# Los sistemas de detección de bots verifican esto en el entorno JS del navegador.
esperar contexto . add_init_script (
"Object.defineProperty(navigator, 'webdriver', {get: () => indefinido})"
)
devolver navegador , contexto
Qué hace esto: La llamada add_init_script() se ejecuta antes de que se ejecute cualquier página JavaScript, lo que significa que la anulación de navigator.webdriver está implementada antes de que el código de detección del sitio pueda verificarlo. El argumento de inicio –disable-blink-features=AutomationControlled elimina un indicador de automatización independiente en el nivel del motor del navegador. Juntos, estos dos cambios manejan los métodos de detección más comunes.
Para sitios con sistemas agresivos de huellas dactilares y CAPTCHA, estas mitigaciones no serán suficientes. Servicios como Browserbase, Spidra y Scraping Browser de Brightdata manejan la resolución de CAPTCHA, la rotación de IP residencial y la administración de huellas digitales del navegador como infraestructura administrada.
2. Espera inteligente
El segundo modo de falla es el tiempo. El reflejo es agregar llamadas time.sleep() y aumentarlas cuando las cosas fallan. Esto es incorrecto en ambas direcciones: demasiado corto en conexiones lentas, demasiado largo en conexiones rápidas y completamente opaco al depurar.
Dramaturgo tiene cuatro estrategias de espera adecuadas. Utilice el que coincida con lo que realmente está esperando:
# Part 2: Smart waiting strategies (add to your scraper or agent tools)
async def smart_wait_examples(page):
“””
Four ways to wait for the right page state, without arbitrary sleeps.
“””
# STRATEGY 1: Wait for a specific element to appear in the DOM
# Use when you know exactly what element signals content has loaded
await page.wait_for_selector(“.product-list”, state=”visible”, timeout=10000)
# STRATEGY 2: Wait for a specific API response
# Use when the content comes from an XHR/fetch call you can identify
async with page.expect_response(
lambda r: “/api/products” in r.url and r.status == 200
) as response_info:
await page.click(“#load-more”)
response = await response_info.value
print(f”API responded: {response.status}”)
# STRATEGY 3: Wait for the URL to change after form submission
# Use when a successful submit redirects to a new page
await page.wait_for_url(“**/dashboard**”, timeout=10000)
# STRATEGY 4: Wait for a JavaScript variable to be set
# Use when no visual element reliably signals the ready state
await page.wait_for_function(
“() => window.__dataLoaded === true”,
timeout=10000
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Part 2: Smart waiting strategies (add to your scraper or agent tools)
async def smart_wait_examples ( page ) :
“” “
Four ways to wait for the right page state, without arbitrary sleeps.
“ “”
# STRATEGY 1: Wait for a specific element to appear in the DOM
# Use when you know exactly what element signals content has loaded
await page . wait_for_selector ( “.product-list” , state = “visible” , timeout = 10000 )
# STRATEGY 2: Wait for a specific API response
# Use when the content comes from an XHR/fetch call you can identify
asíncrono con la página . esperar_respuesta (
lambda r : "/api/productos" en r . URL y r . estado == 200
) como información_respuesta :
espera la página . haga clic en ( "#cargar-más" )
respuesta = aguarde respuesta_info . valor
imprimir ( f "API respondió: {respuesta.status}" )
# ESTRATEGIA 3: Espere a que la URL cambie después del envío del formulario
# Úselo cuando un envío exitoso redirige a una nueva página
espera la página . wait_for_url ( "**/panel**" , tiempo de espera = 10000 )
# ESTRATEGIA 4: Espere a que se establezca una variable de JavaScript
# Úselo cuando ningún elemento visual indique de manera confiable el estado listo
espera la página . esperar_para_función (
"() => ventana.__dataLoaded === verdadero" ,
tiempo de espera = 10000
)
Qué hace esto: cada estrategia está vinculada a un evento observable específico en lugar de a un retraso de tiempo arbitrario. wait_for_selector observa el DOM. expect_response se conecta a la capa de red. wait_for_url monitorea la navegación. wait_for_function evalúa JavaScript en el contexto del navegador. Utilice el que indique más directamente "lo que necesito ya está listo".
3. Persistencia de sesión y cookies
El tercer modo de falla es la pérdida del estado de sesión. Si su agente inicia sesión en un sitio durante el paso uno y luego se destruye el contexto del navegador, el paso dos no tiene autenticación. Volver a crear el inicio de sesión en cada ejecución es lento y puede provocar una limitación o bloqueo de la velocidad.
La solución es guardar las cookies en el disco después de iniciar sesión y cargarlas al inicio de cada ejecución posterior:
# Part 3: Session persistence across runs
COOKIES_FILE = Path(“session_cookies.json”)
async def save_session(context) -> None:
“””Save browser cookies to disk after a successful login.”””
cookies = await context.cookies()
COOKIES_FILE.write_text(json.dumps(cookies, indent=2))
print(f”Session saved: {len(cookies)} cookies written.”)
async def load_session(context) -> bool:
“””Load saved cookies before navigating. Returns True if session was found.”””
if not COOKIES_FILE.exists():
print(“No saved session. Fresh login required.”)
return False
cookies = json.loads(COOKIES_FILE.read_text())
await context.add_cookies(cookies)
print(f”Session restored: {len(cookies)} cookies loaded.”)
return True
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Part 3: Session persistence across runs
COOKIES_FILE = Path ( “session_cookies.json” )
async def save_session ( context ) -> None :
“” “Save browser cookies to disk after a successful login.” “”
cookies = await context . cookies ( )
COOKIES_FILE . write_text ( json . dumps ( cookies , indent = 2 ) )
print ( f “Session saved: {len(cookies)} cookies written.” )
async def load_session ( context ) -> bool :
“” “Load saved cookies before navigating. Returns True if session was found.” “”
if not COOKIES_FILE . exists ( ) :
print ( “No saved session. Fresh login required.” )
return False
cookies = json . loads ( COOKIES_FILE . read_text ( ) )
await context . add_cookies ( cookies )
print ( f “Session restored: {len(cookies)} cookies loaded.” )
return True
Qué hace esto: context.cookies() devuelve todas las cookies para el contexto actual del navegador, incluidos los tokens de sesión y las cookies de autenticación. Escribirlos en JSON y recargarlos en la siguiente ejecución significa que el navegador se inicia en un estado autenticado. Tenga en cuenta que las sesiones caducan; agregue una verificación que recurra a un nuevo inicio de sesión si la sesión guardada devuelve una redirección a la página de inicio de sesión.
Implementación de agentes de navegador
Hacer que un agente de navegador funcione localmente es una cosa. Ejecutarlo de manera confiable en un entorno de nube es otra.
La principal diferencia entre un script de Python que funciona en su computadora portátil y uno que falla en CI son las dependencias del sistema. El navegador Chromium de Playwright requiere un conjunto de bibliotecas compartidas que están presentes en la mayoría de las máquinas de desarrollo, pero que no se encuentran en las imágenes mínimas de la nube. La solución más limpia es Docker.
Dockerfile: cree un contenedor que envíe todo lo que Playwright necesita:
# Dockerfile for headless Playwright-based browser agent
# Build: docker build -t browser-agent .
# Run: docker run –rm -e OPENAI_API_KEY=your_key browser-agent
FROM python:3.11-slim
# Install system dependencies required by Chromium
RUN apt-get update && apt-get install -y
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1
libxrandr2 libgbm1 libasound2 libpangocairo-1.0-0
libpango-1.0-0 libcairo2 libx11-6 libxext6 libxfixes3
fonts-liberation wget ca-certificates
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies first (cached layer — only rebuilds on requirements change)
COPY requirements.txt .
RUN pip install –no-cache-dir -r requirements.txt
# Install Playwright browser binaries into the image
RUN playwright install chromium
RUN playwright install-deps chromium
# Copy application code last (changes here don’t invalidate the pip/playwright layers)
COPY . .
CMD [“python”, “agent_tools.py”]
requirements.txt:
playwright
browser-use
langchain
langchain-openai
langgraph
python-dotenv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Dockerfile for headless Playwright-based browser agent
# Build: docker build -t browser-agent .
# Run: docker run –rm -e OPENAI_API_KEY=your_key browser-agent
FROM python : 3.11 – slim
# Install system dependencies required by Chromium
RUN apt – get update && apt – get install – y
libnss3 libatk1 . 0 – 0 libatk – bridge2 . 0 – 0 libcups2
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1
libxrandr2 libgbm1 libasound2 libpangocairo – 1.0 – 0
libpango – 1.0 – 0 libcairo2 libx11 – 6 libxext6 libxfixes3
fonts – liberation wget ca – certificates
&& rm – rf / var / lib / apt / lists / *
WORKDIR / app
# Install Python dependencies first (cached layer — only rebuilds on requirements change)
COPY requirements . txt .
RUN pip install — no – cache – dir – r requirements . txt
# Install Playwright browser binaries into the image
RUN playwright install chromium
RUN playwright install – deps chromium
# Copy application code last (changes here don’t invalidate the pip/playwright layers)
COPY . .
CMD [ “python” , “agent_tools.py” ]
requirements . txt :
playwright
browser – use
langchain
langchain – openai
langgraph
python – dotenv
Para cargas de trabajo simultáneas que ejecutan varias sesiones de navegador en paralelo, utilice la API asíncrona de Playwright con asyncio.gather():
# Parallel scraping with semaphore rate limiting
# Runs up to 3 browser sessions simultaneously
import asyncio
from playwright.async_api import async_playwright
async def scrape_url(browser, url: str, semaphore: asyncio.Semaphore) -> dict:
“””Scrape a single URL, respecting the concurrency semaphore.”””
async with semaphore:
context = await browser.new_context()
page = await context.new_page()
await page.goto(url, wait_until=”domcontentloaded”)
title = await page.title()
await context.close() # Close context (not browser) to release resources
return {“url”: url, “title”: title}
async def scrape_parallel(urls: list[str], max_concurrent: int = 3) -> list[dict]:
“””Scrape a list of URLs in parallel, capped at max_concurrent sessions.”””
semaphore = asyncio.Semaphore(max_concurrent) # Cap concurrent sessions
async with async_playwright() as p:
# One browser shared across all contexts — much cheaper than one browser per URL
browser = await p.chromium.launch(headless=True)
tasks = [scrape_url(browser, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
await browser.close()
return list(results)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Parallel scraping with semaphore rate limiting
# Runs up to 3 browser sessions simultaneously
import asyncio
from playwright.async_api import async_playwright
async def scrape_url(browser, url: str, semaphore: asyncio.Semaphore) -> dict:
“”“Scrape a single URL, respecting the concurrency semaphore.”“”
async with semaphore:
context = await browser.new_context()
page = await context.new_page()
await page.goto(url, wait_until=“domcontentloaded”)
title = await page.title()
await context.close() # Close context (not browser) to release resources
return {“url”: url, “title”: title}
async def scrape_parallel(urls: list[str], max_concurrent: int = 3) -> list[dict]:
“”“Scrape a list of URLs in parallel, capped at max_concurrent sessions.”“”
semaphore = asyncio.Semaphore(max_concurrent) # Cap concurrent sessions
async with async_playwright() as p:
# One browser shared across all contexts — much cheaper than one browser per URL
browser = await p.chromium.launch(headless=True)
tasks = [scrape_url(browser, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
await browser.close()
devolver lista ( resultados )
Qué hace esto: asyncio.Semaphore(max_concurrent) limita la cantidad de contextos del navegador que se ejecutan al mismo tiempo. Sin él, el inicio de 50 contextos de navegador simultáneos agotará la memoria. Un proceso de navegador se comparte en todos los contextos; un contexto es barato; una instancia de navegador completa no lo es.
En el lado de la infraestructura administrada, Amazon Nova Act se lanzó en marzo de 2025 como un SDK dedicado para crear agentes de navegador en AWS, integrándose de forma nativa con Playwright para el control del navegador. El propio servidor MCP de Playwright brinda a los asistentes de IA control total del navegador a través del protocolo de contexto modelo, utilizando instantáneas de accesibilidad estructuradas en lugar de capturas de pantalla, lo que significa que los costos de los tokens se mantienen bajos mientras que la comprensión de la página por parte del agente se mantiene alta.
Poniéndolo todo junto
Aquí hay un agente completo de extremo a extremo que responde una pregunta de investigación, navega a una fuente de datos pública, extrae resultados estructurados y devuelve un resumen limpio. Utiliza las herramientas del navegador de la Sección 5 orquestadas por un agente de LangGraph.
Cómo ejecutar: guarde como reference_agent.py, asegúrese de que OPENAI_API_KEY esté en su .env y ejecute python reference_agent.py
# reference_agent.py
# Full browser-using AI agent: navigates, extracts, summarizes.
# Target: books.toscrape.com (public scraping sandbox)
# Prerequisites: pip install playwright langchain langchain-openai langgraph python-dotenv
# playwright install chromium
# How to run: python reference_agent.py
import asyncio
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.prebuilt import create_react_agent
from playwright.async_api import async_playwright
load_dotenv()
# ── BROWSER STATE ─────────────────────────────────────────────────────────────
_browser = None
_context = None
_page = None
_playwright = None
async def get_page():
global _browser, _context, _page, _playwright
if _browser is None:
_playwright = await async_playwright().start()
_browser = await _playwright.chromium.launch(headless=True)
_context = await _browser.new_context(
viewport={“width”: 1280, “height”: 720},
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″
)
)
# Remove webdriver fingerprint
await _context.add_init_script(
“Object.defineProperty(navigator, ‘webdriver’, {get: () => undefined})”
)
_page = await _context.new_page()
return _page
async def teardown():
global _browser, _playwright
if _browser:
await _browser.close()
await _playwright.stop()
_browser = None
_playwright = None
# ── TOOLS ─────────────────────────────────────────────────────────────────────
@tool
async def navigate(url: str) -> str:
“””
Navigate the browser to a URL and return the page’s text content.
Use when you need to open a website or move to a new page.
Input: full URL with https:// prefix.
“””
page = await get_page()
await page.goto(url, wait_until=”domcontentloaded”, timeout=20000)
await page.wait_for_load_state(“networkidle”)
content = await page.inner_text(“body”)
return content[:4000]
@tool
async def extract_structured(css_selector: str) -> str:
“””
Extract text from all elements matching a CSS selector on the current page.
Use when you need to pull specific elements from the loaded page.
Input: valid CSS selector string (e.g., ‘h3 a’, ‘.price_color’, ‘article.product_pod’).
“””
page = await get_page()
try:
await page.wait_for_selector(css_selector, timeout=5000)
elements = await page.query_selector_all(css_selector)
texts =[]for el in elements[:20]: # Límite de 20 elementos para mantener la salida manejable text = await el.inner_text() texts.append(text.strip()) return "n".join(texts) if texts else "No se encontraron elementos". excepto excepción como e: return f"Error de extracción: {str(e)}" @tool async def get_current_url() -> str: """Devuelve la URL en la que se encuentra actualmente el navegador. No se requiere entrada.""" page = await get_page() return page.url # ── AGENTE ────────────────────────────────── ─────────────────────────────────── llm = ChatOpenAI( model="gpt-4o", Temperature=0, api_key=os.getenv("OPENAI_API_KEY") ) tools = [navigate, extract_structured, get_current_url] agent = create_react_agent(llm, tools) SYSTEM = ( "Eres un agente de investigación basado en navegador. Tienes acceso a un navegador real. " "Utiliza navegar() para abrir páginas, extract_structured() para extraer elementos específicos, " "y get_current_url() para verificar dónde estás. " "Navega siempre primero, luego extrae. Sea conciso en tu respuesta final." ) async def run_agent(query: str) -> str: result = await agent.ainvoke({ "messages": [ SystemMessage(content=SYSTEM), HumanMessage(content=query) ] }) await desmontaje() return. resultado["mensajes"][-1].content # ── DEMO ─────────────────────────────────── ─────────────────────────────────── if __name__ == "__main__": query = ( "Vaya a https://books.toscrape.com y extraiga los títulos y precios " "de los primeros 5 libros enumerados. Devuélvalos como una lista estructurada." ) print(f"Query: {query}n") respuesta = asyncio.run(run_agent(query)) print(f"Answer:n{answer}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# agente_referencia.py
# Agente de IA que utiliza un navegador completo: navega, extrae y resume.
# Objetivo: books.toscrape.com (zona de pruebas pública de scraping)
# Requisitos previos: pip install dramaturgo langchain langchain-openai langgraph python-dotenv
# dramaturgo instala cromo
# Cómo ejecutar: python reference_agent.py
importar asincio
importar sistema operativo
desde dotenv importar load_dotenv
desde langchain_openai importar ChatOpenAI
de cadena larga . herramienta de importación de herramientas
de langchain_core . los mensajes importan HumanMessage , Mensaje del sistema
de langgraph . importación prediseñada create_react_agent
del dramaturgo . async_api importar async_playwright
cargar_dotenv ( )
# ── ESTADO DEL NAVEGADOR ────────────────────────────── ───────────────────────────────
_navegador = Ninguno
_contexto = Ninguno
_página = Ninguno
_dramaturgo = Ninguno
asíncrono def get_page ( ) :
global _navegador , _contexto , _página , _dramaturgo
si _el navegador es Ninguno :
_dramaturgo = espere async_playwright ( ) . comenzar ( )
_navegador = aguarda _dramaturgo . cromo . lanzamiento ( sin cabeza = Verdadero )
_contexto = espere _navegador . nuevo_contexto (
ventana gráfica = { "ancho" : 1280 , "altura" : 720 } ,
agente_usuario = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, como Gecko) "
"Cromo/120.0.0.0 Safari/537.36"
)
)
# Eliminar la huella digital del controlador web
espera _contexto . add_init_script (
"Object.defineProperty(navigator, 'webdriver', {get: () => indefinido})"
)
_página = espera _contexto . nueva_página ( )
devolver _página
desmontaje asíncrono def ( ) :
global _navegador , _dramaturgo
si _navegador :
espere _navegador . cerca ( )
aguarda _dramaturgo . detener ( )
_navegador = Ninguno
_dramaturgo = Ninguno
# ── HERRAMIENTAS ────────────────────────────────── ───────────────────────────────────
@ herramienta
asíncrono def navegar ( url : cadena ) -> cadena :
"" "
Navegue por el navegador hasta una URL y devuelva el contenido de texto de la página.
Úselo cuando necesite abrir un sitio web o pasar a una nueva página.
Entrada: URL completa con prefijo https://.
" ""
página = espera get_page ( )
espera la página . ir a ( url , wait_until = "domcontentloaded" , tiempo de espera = 20000 )
espera la página . wait_for_load_state ( "red inactiva" )
contenido = espera la página . texto_interior ( "cuerpo" )
devolver contenido [ : 4000 ]
@ herramienta
async def extract_structured ( css_selector : cadena ) -> cadena :
"" "
Extraiga texto de todos los elementos que coincidan con un selector CSS en la página actual.
Úselo cuando necesite extraer elementos específicos de la página cargada.
Entrada: cadena de selector CSS válida (p. ej., 'h3 a', '.price_color', 'article.product_pod').
" ""
página = espera get_page ( )
intentar :
espera la página . esperar_para_selector ( css_selector , tiempo de espera = 5000 )
elementos = espera la página . consulta_selector_all ( css_selector )
textos = [ ]
para el en elementos [ : 20 ] : # Límite de 20 elementos para mantener la producción manejable
texto = espera el . texto_interior ( )
textos . agregar ( texto . strip ( ) )
devolver "n" . unirse ( textos ) si textos más "No se encontraron elementos."
excepto excepción como mi :
devolver f "Falló la extracción: {str(e)}"
@ herramienta
asíncrono def get_current_url ( ) -> cadena :
"" "Devuelve la URL en la que se encuentra actualmente el navegador. No se requiere entrada." ""
página = espera get_page ( )
devolver página . URL
# ── AGENTE ────────────────────────────────── ───────────────────────────────────
llm = ChatOpenAI (
modelo = "gpt-4o" ,
temperatura = 0 ,
api_key = sistema operativo . getenv ( "OPENAI_API_KEY" )
)
herramientas = [ navegar , extraer_estructurado , get_current_url ]
agente = create_react_agent ( llm , herramientas )
SISTEMA = (
"Usted es un agente de investigación basado en un navegador. Tiene acceso a un navegador real".
"Utilice navegar() para abrir páginas, extract_structured() para extraer elementos específicos",
"y get_current_url() para comprobar dónde estás."
"Navega siempre primero y luego extrae. Sea conciso en su respuesta final".
)
async def run_agent ( consulta : cadena ) -> cadena :
resultado = A la espera del agente . invocar ( {
"mensajes" : [
Mensaje del sistema ( contenido = SISTEMA ) ,
HumanMessage ( contenido = consulta )
]
} )
esperando desmontaje ( )
devolver resultado [ "mensajes" ] [ – 1 ] . contenido
# ── DEMOSTRACIÓN ─────────────────────────────────── ───────────────────────────────────
si __nombre__ == "__principal__" :
consulta = (
"Vaya a https://books.toscrape.com y extraiga los títulos y precios"
"de los primeros 5 libros enumerados. Devuélvalos como una lista estructurada".
)
imprimir ( f "Consulta: {consulta}n" )
respuesta = asincio . ejecutar ( run_agent ( consulta ) )
imprimir ( f "Respuesta:n{respuesta}" )
Qué hace esto: este agente tiene tres herramientas limpias: navegar, extraer_estructurado y obtener_actual_url, además de un mensaje del sistema que le indica exactamente cuándo usar cada una. El agente llama a navegar para cargar la página, a extract_structured para extraer los títulos y precios de los libros mediante el selector CSS y sintetiza una lista estructurada en la respuesta final. La llamada a Teardown() después de que el agente finaliza cierra el navegador limpiamente para que no queden ejecutándose procesos zombies de Chromium.
Conclusión
El navegador no es una herramienta especializada para ingenieros de automatización. Es la interfaz universal para la web, y la web es donde se realiza la mayor parte del trabajo real del mundo. Un agente de IA que puede utilizar un navegador no necesita un equipo de socios que mantenga las integraciones de API. Puede alcanzar cualquier cosa que un humano pueda alcanzar.
Lo que hace que esto sea práctico ahora, y no sólo teóricamente interesante, es la madurez de las herramientas. Playwright maneja las partes difíciles de la interacción del navegador. El uso del navegador elimina la necesidad de escribir selectores para tareas exploratorias. LangGraph le brinda al LLM herramientas limpias y un bucle de razonamiento que maneja estructuras de página variables. Los patrones de este artículo no son demostraciones. Son los mismos patrones que el 51% de las empresas que ahora utilizan agentes de IA en producción están construyendo.
Comience con el ejemplo de raspado. Ejecútelo en un sitio del que realmente necesite datos. Agregue la capa de agente cuando necesite decisiones que el script no pueda anticipar. Agregue el uso del navegador cuando la estructura de la página sea demasiado dinámica para los selectores. Implemente en Docker cuando necesite que se ejecute en otro lugar que no sea su computadora portátil.
La parte difícil no es el código. Es saber qué herramienta alcanzar en cada capa. Esperemos que este artículo lo haya dejado más claro.