Selenium controla un navegador real desde Python, lo que lo convierte en la herramienta a la que recurres cuando requests te devuelve una cáscara vacía y los datos que necesitas solo aparecen después de que se ejecuta JavaScript. Esta guía cubre el web scraping con Selenium tal como funciona en 2026: Selenium 4 con gestión de driver integrada (sin descargas de chromedriver), el nuevo modo headless, localizadores By. y esperas explícitas que reemplazan las conjeturas de time.sleep. Todos los ejemplos son ejecutables tal cual.
Si eres nuevo en el scraping en general, empieza con nuestra guía principal de web scraping con Python — este artículo asume que sabes qué es un selector CSS y retoma el tema justo donde termina el HTML estático y comienza un navegador headless.
Cuándo realmente necesitas Selenium
El terreno natural de Selenium es la automatización de navegadores y las pruebas, y es genuinamente bueno en eso: habla el estándar W3C WebDriver, controla Chrome/Firefox/Edge reales y tiene dos décadas de cicatrices de batalla. Para scraping, ese poder solo vale la pena en situaciones específicas:
| Situación | Herramienta correcta |
|---|---|
Los datos están en el HTML inicial (view-source los muestra) | requests + BeautifulSoup — consulta nuestro tutorial de BeautifulSoup |
| Los datos llegan desde una API JSON que llama la página | requests apuntando directamente a esa API (revisa primero la pestaña Network) |
| Los datos se renderizan solo después de que se ejecuta JavaScript | Selenium o Playwright |
| Los datos aparecen solo después de hacer clic, desplazar o escribir | Selenium o Playwright |
| Necesitas capturas de pantalla o renderizado de PDF | Selenium o Playwright |
Las dos primeras filas importan más de lo que parece. Un navegador real gasta segundos y cientos de megabytes por página; una simple solicitud HTTP gasta milisegundos. Antes de escribir cualquier línea de Selenium, abre DevTools, observa la pestaña Network y verifica si los datos «solo con JavaScript» en realidad están en un endpoint JSON ordenado al que puedas llamar directamente. Aproximadamente la mitad de las veces lo está — y ese es todo el trabajo del kit de herramientas de web scraping con Python. Recurre a Selenium cuando la respuesta sea genuinamente no.
Configuración en 2026: un pip install, sin descargas de driver
Esta parte mejoró dramáticamente. Desde Selenium 4.6, Selenium Manager viene incluido dentro del paquete: detecta tu navegador instalado, descarga el binario del driver correspondiente y lo guarda en caché. El viejo ritual — descargar chromedriver, hacer coincidir la versión, arreglar tu PATH o añadir webdriver-manager — ha desaparecido. Si un tutorial te dice que instales webdriver-manager, está desactualizado.
python -m venv venv && source venv/bin/activate
pip install selenium
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--headless=new") # the current headless mode
options.add_argument("--window-size=1920,1080") # default is 800x600 — breaks responsive layouts
driver = webdriver.Chrome(options=options) # Selenium Manager fetches the driver on first run
driver.get("https://www.selenium.dev")
print(driver.title)
driver.quit()
Dos flags que vale la pena explicar:
--headless=newes el modo headless moderno de Chrome — comparte mucho más código con Chrome en modo visible que el antiguo flag--headless, así que las páginas se renderizan de la misma forma que en tu pantalla. Úsalo en servidores y CI; quítalo mientras depuras para poder ver al navegador trabajar.--window-size=1920,1080importa porque el modo headless usa por defecto 800x600, y los sitios responsivos silenciosamente te sirven el diseño móvil — entonces tus selectores de escritorio no coinciden con nada.
El flujo de trabajo central de scraping
El ciclo es siempre el mismo: navegar, esperar el contenido, encontrar elementos con localizadores By., extraer texto y atributos en diccionarios simples. Aquí está aplicado contra quotes.toscrape.com/js, un sitio de práctica que renderiza todo con JavaScript (un simple requests.get devuelve cero citas):
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
try:
driver.get("https://quotes.toscrape.com/js/")
# Block until at least one quote card exists in the DOM (max 10s)
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".quote"))
)
quotes = []
for card in driver.find_elements(By.CSS_SELECTOR, ".quote"):
quotes.append({
"text": card.find_element(By.CSS_SELECTOR, ".text").text,
"author": card.find_element(By.CSS_SELECTOR, ".author").text,
"tags": [t.text for t in card.find_elements(By.CSS_SELECTOR, ".tag")],
})
print(f"scraped {len(quotes)} quotes")
print(quotes[0])
finally:
driver.quit()
Notas sobre la API moderna, ya que aún circulan muchos tutoriales desactualizados:
- Los localizadores son
find_element(By.CSS_SELECTOR, "..."). Los antiguos helpersfind_element_by_css_selectorse eliminaron en Selenium 4.3 — el código que los usa hoy lanzaAttributeError.By.CSS_SELECTORcubre casi todo;By.IDyBy.XPATHllenan los huecos (XPath es el que puede seleccionar por contenido de texto). find_elementvsfind_elements: la forma singular lanzaNoSuchElementExceptioncuando nada coincide; la plural devuelve una lista vacía. Usa la plural para grillas de tarjetas, luego la singular acotada a cada tarjeta — llamar acard.find_element(...)busca solo dentro de esa tarjeta.- Los atributos vienen de
element.get_attribute("href")(o"src","data-id", ...); el texto visible deelement.text. try/finallycondriver.quit()— de lo contrario cada ejecución que falla deja escapar un proceso de Chrome, y diez ejecuciones fallidas después tu máquina no estará contenta.
Interacciones reales: clic, scroll, escritura
La razón por la que aceptaste la sobrecarga de Selenium es la interacción. Tres patrones cubren la mayoría de los sitios.
Hacer clic en «cargar más» hasta que desaparezca. Espera a que sea clicable, haz clic, repite; cuando el botón desaparece, el bucle termina:
from selenium.common.exceptions import TimeoutException
while True:
try:
button = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button.load-more"))
)
button.click()
except TimeoutException:
break # no more button — everything is loaded
Desplazar un feed infinito. Desplázate hasta el fondo, deja que el nuevo contenido aterrice y detente cuando la altura de la página deje de crecer:
last_height = driver.execute_script("return document.body.scrollHeight")
while True:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
try:
WebDriverWait(driver, 5).until(
lambda d: d.execute_script("return document.body.scrollHeight") > last_height
)
last_height = driver.execute_script("return document.body.scrollHeight")
except TimeoutException:
break # height stopped growing — feed exhausted
Llenar un cuadro de búsqueda. send_keys escribe como un usuario, Keys.RETURN envía:
from selenium.webdriver.common.keys import Keys
box = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "input[name='q']"))
)
box.clear()
box.send_keys("mechanical keyboard", Keys.RETURN)
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".result"))
)
Después de cualquier interacción, espera su efecto (nuevas tarjetas, altura cambiada, un contenedor de resultados) antes de extraer — que el clic haya retornado no significa que la página haya terminado de reaccionar.
Fallos comunes y cómo solucionarlos
Todo scraper de Selenium eventualmente se encuentra con estos tres. La solución casi nunca es «agregar un sleep»:
| Excepción | Qué significa | Solución |
|---|---|---|
StaleElementReferenceException | Mantuviste una referencia a un elemento y luego la página lo volvió a renderizar (común después de clics y actualizaciones AJAX). La referencia antigua apunta a un nodo del DOM que ya no existe. | Vuelve a buscar el elemento después de cualquier acción que modifique la página. En bucles, vuelve a consultar find_elements en cada iteración en lugar de iterar sobre una lista guardada. |
ElementNotInteractableException / ElementClickInterceptedException | El elemento existe pero está oculto, tiene tamaño cero o está cubierto por algo más (banner de cookies, encabezado fijo, modal). | Espera element_to_be_clickable, descarta primero el overlay, o usa driver.execute_script("arguments[0].scrollIntoView();", el) antes de hacer clic. |
TimeoutException | Tu condición de WebDriverWait nunca se volvió verdadera. O el selector está mal, el contenido carga dentro de un iframe, o el sitio te sirvió una página de bloqueo. | Imprime driver.title y guarda driver.page_source cuando se dispare — a menudo encontrarás un CAPTCHA o una página de «verifica que eres humano» en lugar del contenido. Revisa si hay iframes con driver.switch_to.frame(...). |
NoSuchElementException | find_element (singular) no encontró nada — a menudo las mismas causas raíz de arriba, sin la parte de la espera. | Espera primero, luego busca; o usa find_elements y maneja la lista vacía. |
Esa tercera fila merece énfasis: un TimeoutException frecuentemente es un problema de detección disfrazado de problema de selector. Lo cual nos lleva a la sección honesta.
La parte honesta: Selenium es lento y detectable
Cada navegador real trae dos costos consigo.
Velocidad y peso. Una sesión de navegador cuesta segundos por página y aproximadamente 300-500 MB de RAM, mientras que una solicitud HTTP cuesta milisegundos y casi nada. En diez páginas eso es irrelevante; en cien mil páginas es una flota de servidores y un sistema de colas que ahora tienes que mantener.
Detección. De fábrica, Chrome controlado por Selenium se delata a sí mismo: navigator.webdriver es true, y docenas de señales más sutiles — el handshake TLS, plugins faltantes, peculiaridades de canvas y fuentes, tu IP de datacenter — alimentan sistemas de huella digital del navegador que te puntúan como bot antes de que corra tu primer selector. Los síntomas son limitación de velocidad, murallas interminables de CAPTCHA, o datos basura silenciosos.
Herramientas como undetected-chromedriver y el modo UC de SeleniumBase parchan las señales más ruidosas, y funcionan — hasta que el proveedor lanza una nueva verificación, lo cual hacen continuamente, porque detectar la automatización es todo su negocio. Es una carrera armamentista en la que tú eres el aficionado y ellos son el contratista de defensa. Escribimos el panorama completo en sitios de scraping que bloquean bots; la versión corta es que parchar tu navegador es una cinta de correr, no un destino.
¿Qué pasa con Playwright? Playwright es la alternativa más joven: localizadores con auto-espera integrados, más rápido y estable en operación headless, soporte async, y una API más agradable para scraping específicamente. Selenium sigue ganando cuando necesitas la matriz más amplia de navegadores reales, el estándar WebDriver, una década de respuestas en Stack Overflow, o bindings más allá de los cuatro lenguajes principales — y sigue siendo la opción por defecto en los equipos de QA por buenas razones. Para un proyecto nuevo enfocado solo en scraping, normalmente empezaríamos con Playwright; para automatización que debe reflejar la cobertura real de usuarios en distintos navegadores, Selenium. Ninguno es menos detectable, sin embargo — a un sistema de fingerprinting no le importa qué librería mueve el mouse.
Escalando: cuándo una API estructurada supera a una flota de navegadores
Aquí está la decisión que realmente importa a escala. Si tu objetivo es una plataforma grande y bien defendida — Amazon, Google, redes sociales — el camino de Selenium se convierte en: proxies, parches de fingerprint, manejo de CAPTCHA, lógica de reintentos, parsers de HTML que se rompen con cada rediseño, y servidores para correr los navegadores. Cada pieza es factible; juntas son un trabajo de infraestructura a tiempo parcial.
La alternativa es una API de web scraping que corre esa infraestructura detrás de un endpoint y devuelve JSON estructurado. La misma búsqueda en Amazon que toma ~40 líneas de Selenium, esperas y parsing es una sola solicitud HTTP:
import requests
resp = requests.get(
"https://api.crawlora.net/api/v1/amazon/search",
params={"k": "mechanical keyboard"},
headers={"x-api-key": "YOUR_API_KEY"},
)
products = resp.json()["data"] # structured fields — no parser to maintain
Sin navegador, sin driver, sin selectores que cuidar — y Crawlora cobra por éxito, lo que significa que solo se te cobra por respuestas 2xx exitosas, así que una solicitud bloqueada o fallida no te cuesta nada. La división sensata a la que llega la mayoría de los equipos: Selenium para los sitios donde necesitas interacción personalizada o donde genuinamente es la herramienta de automatización de navegador correcta, una API estructurada para las plataformas de alto volumen y fuertemente defendidas. Puedes comparar los costos tú mismo — la matemática de una flota de navegadores frente a precios por solicitud — y probar los endpoints en el playground antes de escribir código.
- Revisé primero la pestaña Network — ¿los datos ya están en un endpoint JSON al que requests puede acceder?
- pip install selenium (4.6+) y dejar que Selenium Manager maneje los drivers — sin descargas de chromedriver
- Ejecutando --headless=new con un --window-size explícito
- Cada espera es WebDriverWait + expected_conditions — cero llamadas a time.sleep
- driver.quit() en un bloque finally para que las ejecuciones fallidas no dejen escapar navegadores
- En TimeoutException, volqué page_source para revisar páginas de bloqueo antes de culpar al selector
- Honesto sobre la escala: interacciones personalizadas en Selenium, plataformas protegidas de alto volumen vía una API estructurada
Sáltate la flota de navegadores para los objetivos difíciles
JSON estructurado desde Amazon, Google y más de 700 endpoints — proxies, renderizado y reintentos gestionados, cobrado solo en respuestas 2xx exitosas. 2,000 créditos/mes, sin tarjeta.
Lecturas relacionadas
- Web scraping con Python: la guía completa — el pilar: requests, parsing, almacenamiento y cuándo escalar a un navegador
- Tutorial de BeautifulSoup — el camino más rápido cuando el HTML es estático
- Web scraping con Playwright — la alternativa moderna de automatización de navegador comparada arriba
- Sitios de scraping que bloquean bots — la carrera armamentista de detección al completo
Preguntas frecuentes
Is Selenium good for web scraping?
Yes, when the data only appears after JavaScript runs or after an interaction like clicking or scrolling — Selenium drives a real browser, so it sees exactly what a user sees. For static HTML it is overkill: a plain HTTP request with requests and BeautifulSoup is 10-50x faster and far lighter.
Selenium vs BeautifulSoup — which should I use?
They solve different problems. BeautifulSoup parses HTML you already fetched; Selenium runs a browser to produce that HTML in the first place. If the data is in the page source or a JSON endpoint, use requests plus BeautifulSoup. If it renders client-side or requires clicks and scrolls, use Selenium — and you can still hand driver.page_source to BeautifulSoup for parsing.
Do I still need to download chromedriver for Selenium?
No. Since Selenium 4.6, the built-in Selenium Manager detects your installed browser and downloads the matching driver automatically. pip install selenium is the entire setup — separate chromedriver downloads and the webdriver-manager package are obsolete.
Can websites detect Selenium?
Yes. Selenium-driven Chrome sets navigator.webdriver to true, and browser fingerprinting systems read many subtler signals — TLS handshake, missing plugins, canvas quirks, datacenter IPs. Patches like undetected-chromedriver hide the loudest tells, but anti-bot vendors update their checks continuously, so evasion is an ongoing arms race rather than a one-time fix.
How do I wait for a page to load in Selenium?
Use WebDriverWait with expected_conditions instead of time.sleep. WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, ".quote"))) polls until the element exists and returns immediately when it does — faster on fast pages, tolerant on slow ones. Use element_to_be_clickable before clicks and visibility_of_element_located when you need rendered content.
Is Selenium or Playwright better for web scraping?
For a new scraping-only project, Playwright usually wins: auto-waiting locators, faster headless runs, and async support. Selenium wins on the broadest real-browser coverage, the W3C WebDriver standard, and its ecosystem, which is why QA teams still default to it. Neither is less detectable — anti-bot systems fingerprint the browser, not the library driving it.
Why is my Selenium scraper getting TimeoutException?
A TimeoutException means your wait condition never became true. Common causes: a wrong selector, content inside an iframe you have not switched to, or — very often — the site served a block page or CAPTCHA instead of the content. Dump driver.page_source when it fires; if you see an anti-bot page, the problem is detection, not your selector.
