¿Se puede hacer scraping de un sitio protegido por CAPTCHA sin abrir un navegador? En la mayoría de los casos, sí. Si el formulario funciona con peticiones HTTP normales, basta con combinar requests, BeautifulSoup y un servicio como CaptchaAI: envías el desafío a la API, recibes el token y lo incluyes en el POST, sin Selenium ni navegador headless.
Es el patrón ideal cuando, por ejemplo, monitorizas precios en un marketplace como MercadoLibre o Amazon.es y un reCAPTCHA aparece cada cierto número de solicitudes. Resolverlo por API mantiene el scraper ligero y con un costo predecible, siempre que respetes los términos de servicio del sitio y la normativa de protección de datos aplicable (GDPR y LOPDGDD en España, LFPDPPP en México). Esta guía cubre reCAPTCHA v2 y v3, Cloudflare Turnstile y los CAPTCHA de imagen, todos con la misma clase auxiliar.
Qué necesitas antes de empezar
- Python 3.7+ con pip.
- requests —
pip install requests. - beautifulsoup4 —
pip install beautifulsoup4. - Una clave API de CaptchaAI, disponible en captchaai.com.
Una clase reutilizable para resolver CAPTCHA
El núcleo de un scraper con CAPTCHA es una clase que encapsule el envío a la API y el sondeo del resultado:
import requests
import time
class CaptchaSolver:
def __init__(self, api_key):
self.api_key = api_key
self.base = "https://ocr.captchaai.com"
def _submit(self, params):
params["key"] = self.api_key
resp = requests.get(f"{self.base}/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit error: {resp.text}")
return resp.text.split("|")[1]
def _poll(self, task_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(5)
resp = requests.get(f"{self.base}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id
})
if resp.text == "CAPCHA_NOT_READY":
continue
if resp.text.startswith("OK|"):
return resp.text.split("|")[1]
raise Exception(f"Solve error: {resp.text}")
raise TimeoutError("Solve timed out")
def solve_recaptcha_v2(self, site_key, page_url):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_recaptcha_v3(self, site_key, page_url, action="verify"):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"version": "v3",
"action": action
})
return self._poll(task_id)
def solve_turnstile(self, site_key, page_url):
task_id = self._submit({
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_image(self, image_base64):
task_id = self._submit({
"method": "base64",
"body": image_base64
})
return self._poll(task_id)
La clase separa dos responsabilidades. _submit envía la tarea a in.php y devuelve el id; _poll consulta res.php cada cinco segundos hasta que el resultado está listo o vence el timeout. Los métodos públicos (solve_recaptcha_v2, solve_recaptcha_v3, solve_turnstile, solve_image) solo cambian los parámetros del envío, así que añadir un tipo nuevo es cuestión de una función más. Fíjate en dos detalles: reCAPTCHA v3 necesita además version y action, y Turnstile usa sitekey en lugar de googlekey.
Scraping de un formulario protegido con reCAPTCHA
El flujo para reCAPTCHA v2 son cinco pasos: cargar la página, extraer el sitekey, resolver, enviar el token en g-recaptcha-response y analizar la respuesta.
from bs4 import BeautifulSoup
import requests
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
# Step 1: Load the page
url = "https://example.com/search"
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Step 2: Extract the site key
recaptcha_div = soup.find("div", class_="g-recaptcha")
site_key = recaptcha_div["data-sitekey"]
# Step 3: Solve the CAPTCHA
token = solver.solve_recaptcha_v2(site_key, url)
# Step 4: Submit the form with the token
form_data = {
"q": "search term",
"g-recaptcha-response": token
}
result = session.post(url, data=form_data)
# Step 5: Parse the results
result_soup = BeautifulSoup(result.text, "html.parser")
items = result_soup.find_all("div", class_="result-item")
for item in items:
print(item.text.strip())
El punto clave es el orden: primero extraes el data-sitekey del div.g-recaptcha, luego resuelves y solo entonces envías el POST con el token. Si inviertes los pasos o reutilizas un token viejo, el servidor volverá a devolver la página con el CAPTCHA, porque el token de reCAPTCHA caduca en unos dos minutos. Mantén todo dentro de la misma requests.Session() para conservar las cookies que muchos formularios asocian a la verificación.
Cómo recorrer resultados paginados detrás de un CAPTCHA
Cuando los resultados se paginan, resuelve el CAPTCHA en cada iteración y añade una pausa entre solicitudes para no saturar el servidor:
def scrape_all_pages(base_url, site_key, max_pages=10):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
all_results = []
for page_num in range(1, max_pages + 1):
page_url = f"{base_url}?page={page_num}"
# Solve CAPTCHA for each page if needed
token = solver.solve_recaptcha_v2(site_key, page_url)
resp = session.get(page_url, params={
"g-recaptcha-response": token,
"page": page_num
})
soup = BeautifulSoup(resp.text, "html.parser")
items = soup.find_all("div", class_="item")
if not items:
break
all_results.extend([item.text.strip() for item in items])
print(f"Page {page_num}: {len(items)} items")
time.sleep(2) # Polite delay
return all_results
Resolver un CAPTCHA por página multiplica el consumo de threads, así que conviene comprobar primero si cada página realmente lo exige antes de llamar al solver. La pausa con time.sleep no es un adorno: un ritmo constante y humano reduce la probabilidad de que el sitio active defensas adicionales o te devuelva resultados vacíos.
Resolver CAPTCHA de imagen y texto
Para el clásico CAPTCHA de imagen con texto distorsionado, descarga la imagen, codifícala en base64 y envíala al método de OCR:
import base64
def scrape_with_image_captcha(url):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Find the CAPTCHA image
captcha_img = soup.find("img", {"id": "captcha-image"})
captcha_url = captcha_img["src"]
# Download and encode the image
img_resp = session.get(captcha_url)
img_base64 = base64.b64encode(img_resp.content).decode()
# Solve
captcha_text = solver.solve_image(img_base64)
# Submit
form_data = {
"captcha": captcha_text,
"username": "user"
}
result = session.post(url, data=form_data)
return result.text
A diferencia de reCAPTCHA o Turnstile, aquí el resultado es la cadena de texto leída de la imagen, no un token: va directa al campo del formulario que corresponda. Es el patrón habitual para portales antiguos y formularios de login sencillos.
Reintentos y manejo de errores para producción
En un scraper real la red falla y algún token caduca; envuelve la resolución en una función con reintentos para que un fallo puntual no detenga el proceso:
def solve_with_retry(solver, site_key, page_url, max_retries=3):
for attempt in range(max_retries):
try:
return solver.solve_recaptcha_v2(site_key, page_url)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(2)
Distingue el tipo de error antes de reintentar: no tiene sentido repetir un ERROR_WRONG_USER_KEY o un ERROR_ZERO_BALANCE, que solo se resuelven revisando la clave o el saldo, pero sí un ConnectionError puntual. Para estos últimos, combina el reintento con un retroceso exponencial para no golpear la API cuando la red se degrada.
Buenas prácticas para un scraper estable
- Reutiliza una sola
requests.Session()por dominio para conservar cookies y cabeceras entre solicitudes. - Llama al solver solo cuando la página muestre realmente un CAPTCHA, no en cada solicitud: ahorra threads y tiempo.
- Usa el token en cuanto lo recibas, porque caduca en un par de minutos.
- Registra los códigos de error de la API para detectar a tiempo un saldo agotado o una clave inválida.
Errores frecuentes y cómo resolverlos
| Problema | Causa | Solución |
|---|---|---|
ERROR_WRONG_USER_KEY |
Clave API no válida | Verifica la clave desde el panel de control |
ERROR_ZERO_BALANCE |
Sin saldo | Recarga tu cuenta |
| El formulario vuelve a devolver la página con CAPTCHA | Token caducado o campo incorrecto | Usa el token de inmediato y revisa los nombres de los campos |
ConnectionError |
Problema de red | Añade reintentos con retroceso exponencial |
| Resultados vacíos tras el envío | El sitio necesita cookies o sesión | Usa requests.Session() para conservar las cookies |
Preguntas frecuentes
¿Puedo resolver reCAPTCHA v3 sin abrir un navegador?
Sí. reCAPTCHA v3 no muestra ningún desafío visual: se basa en una puntuación, y CaptchaAI devuelve un token válido a partir del sitekey y la URL. Lo envías con requests, sin renderizar la página.
¿Cuánto cuesta resolver CAPTCHA a gran volumen?
CaptchaAI cobra por thread concurrente, no por resolución, y cada plan incluye resoluciones ilimitadas. El plan BASIC cuesta $15/mes con 5 threads; ADVANCE ofrece $90/mes con 50 threads. Es un costo mensual predecible en USD.
¿Puedo integrar la resolución de forma asíncrona?
Sí. Para lanzar muchas resoluciones en paralelo, usa aiohttp con la API en lugar de requests. Tienes el patrón completo en la guía de integración asíncrona con aiohttp.
¿Por qué el sitio me sigue bloqueando aunque el token sea válido?
El token resuelve el CAPTCHA, pero no oculta un patrón de tráfico agresivo. Espacia las solicitudes, usa encabezados realistas y reparte la carga entre varias salidas de red autorizadas. Más detalle en la rotación de IP para scraping con CAPTCHA.
¿Con qué tipos de CAPTCHA funciona este método?
Con los que resuelve CaptchaAI a través de la API: reCAPTCHA v2 y v3 (incluida la variante Enterprise), Cloudflare Turnstile y Challenge, GeeTest v3 y los CAPTCHA de imagen y de texto por OCR. Basta con cambiar el method del envío y ajustar los parámetros. CaptchaFox, Friendly Captcha y Lemin están disponibles en beta.