Para automatizar un formulario protegido por CAPTCHA solo hay que ordenar bien tres pasos: rellenar los campos, resolver el desafío y escribir el token en el campo oculto correcto antes de pulsar enviar. Con Selenium y la API de CaptchaAI puedes montar ese flujo una sola vez y reutilizarlo en formularios de contacto, registro o inicio de sesión sin tocar el navegador a mano. En esta guía construimos ese automatizador pieza a pieza, con código en Python que detecta el tipo de CAPTCHA sobre la marcha.
Por qué los formularios con CAPTCHA frenan la automatización
Un formulario con CAPTCHA no acepta el envío hasta que demuestres que hay una persona detrás. Ese es justo el punto donde se rompe un script de QA: el navegador rellena los campos, pulsa enviar y el servidor responde con un error de validación porque falta el token del CAPTCHA.
El caso típico en equipos de habla hispana es el de una agencia que necesita validar decenas de veces al día el formulario de contacto de un cliente antes de cada despliegue, o el de un portal de trámites (tipo cita previa) que hay que monitorizar de forma autorizada. En todos ellos el patrón es el mismo: sin resolver el CAPTCHA, no hay envío que valga. La solución es delegar esa parte en un servicio de resolución y quedarte con un flujo predecible en Python.
Arquitectura del envío de formularios automatizado
El flujo es lineal: Selenium carga la página, rellena los campos, detecta y resuelve el CAPTCHA, y solo entonces envía. Resolver el desafío en el último momento evita que el token caduque antes de llegar al servidor.
┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ Load Form │────▶│ Fill Fields │────▶│ Detect & │────▶│ Submit Form │
│ (Selenium) │ │ │ │ Solve │ │ │
│ │ │ │ │ CAPTCHA │ │ │
└────────────┘ └──────────────┘ └────────────┘ └──────────────┘
Las tres piezas para automatizar formularios con CAPTCHA
El automatizador se apoya en tres clases con responsabilidades separadas: una habla con la API, otra reconoce el CAPTCHA en la página y la tercera orquesta el envío. Definirlas en este orden importa, porque cada una depende de la anterior.
El solver que habla con la API de CaptchaAI
Esta clase envía la tarea al endpoint in.php, espera y sondea el resultado en res.php hasta obtener el token. El sondeo respeta el estado CAPCHA_NOT_READY y reintenta cada pocos segundos en lugar de golpear la API sin pausa.
import time
import requests
class FormCaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(f"Submit error: {resp['request']}")
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(f"Solve error: {result['request']}")
raise TimeoutError("CAPTCHA solve timed out")
El detector que identifica el tipo de CAPTCHA
Antes de resolver nada hay que saber qué se está resolviendo. El detector inspecciona el DOM y devuelve el tipo junto con el dato que necesita el solver: el sitekey para Turnstile y reCAPTCHA, o la URL de la imagen para un CAPTCHA de texto. El orden de comprobación cuenta: Turnstile primero, luego reCAPTCHA v2 y por último la imagen.
import re
from selenium.webdriver.common.by import By
class CaptchaDetector:
def __init__(self, driver):
self.driver = driver
def detect(self):
"""Detect CAPTCHA type on current page."""
html = self.driver.page_source
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile, [data-sitekey]")
for el in turnstile:
if "cf-turnstile" in (el.get_attribute("class") or ""):
return "turnstile", el.get_attribute("data-sitekey")
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha:
sitekey = recaptcha[0].get_attribute("data-sitekey")
if "recaptcha" in html.lower():
return "recaptcha_v2", sitekey
# Image CAPTCHA
img = self.driver.find_elements(By.CSS_SELECTOR, "img[src*='captcha'], img.captcha")
if img:
return "image", img[0].get_attribute("src")
return "none", None
El automatizador que rellena y envía
La tercera clase une las dos anteriores. Rellena cada campo con esperas explícitas, llama a solve_captcha() y escribe el token en el campo oculto correspondiente: g-recaptcha-response para reCAPTCHA, cf-turnstile-response para Turnstile. Fíjate en que cada tipo usa su propio nombre de campo; confundirlos es una de las causas más habituales de tokens rechazados.
import base64
import requests as req
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
class FormAutomator:
def __init__(self, api_key):
self.solver = FormCaptchaSolver(api_key)
self.driver = webdriver.Chrome()
self.detector = CaptchaDetector(self.driver)
def fill_field(self, selector, value):
field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
field.clear()
field.send_keys(value)
def select_option(self, selector, value):
from selenium.webdriver.support.ui import Select
select = Select(self.driver.find_element(By.CSS_SELECTOR, selector))
select.select_by_value(value)
def solve_captcha(self):
captcha_type, data = self.detector.detect()
page_url = self.driver.current_url
if captcha_type == "recaptcha_v2":
token = self.solver.solve({
"method": "userrecaptcha",
"googlekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
return True
if captcha_type == "turnstile":
token = self.solver.solve({
"method": "turnstile",
"sitekey": data,
"pageurl": page_url,
})
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return True
if captcha_type == "image":
img_data = req.get(data).content
img_b64 = base64.b64encode(img_data).decode()
text = self.solver.solve({"method": "base64", "body": img_b64})
captcha_input = self.driver.find_element(
By.CSS_SELECTOR, "input[name*='captcha']"
)
captcha_input.clear()
captcha_input.send_keys(text)
return True
return False # No CAPTCHA detected
def submit_form(self, url, fields, submit_selector="button[type='submit']"):
"""
fields: list of (selector, value) tuples
"""
self.driver.get(url)
for selector, value in fields:
self.fill_field(selector, value)
self.solve_captcha()
submit = self.driver.find_element(By.CSS_SELECTOR, submit_selector)
submit.click()
return self.driver.current_url
def close(self):
self.driver.quit()
Ejemplo: QA automatizado de un formulario de contacto
Con las tres piezas montadas, automatizar un formulario de contacto se reduce a una llamada. Este es el escenario más común para un equipo de QA: cargar la página, rellenar nombre, correo y mensaje, dejar que el automatizador resuelva el CAPTCHA y comprobar la URL de redirección para saber si el envío se aceptó.
automator = FormAutomator("YOUR_API_KEY")
try:
result_url = automator.submit_form(
url="https://example.com/contact",
fields=[
("#name", "John Doe"),
("#email", "john@example.com"),
("#subject", "Sales inquiry"),
("#message", "I'd like to learn more about your services."),
],
submit_selector="#submit-btn",
)
print(f"Form submitted. Redirected to: {result_url}")
finally:
automator.close()
Reutilizar el flujo en otros formularios con CAPTCHA
La ventaja de encapsular la lógica en FormAutomator es que el mismo objeto sirve para cualquier formulario: solo cambian la URL, la lista de campos y el selector del botón. Usa siempre entornos de staging propios o autorizados, y respeta los términos de servicio de cada sitio.
Formulario de inicio de sesión
result = automator.submit_form(
url="https://staging.example.com/qa-login",
fields=[
("#username", "testuser"),
("#password", "testpass123"),
],
submit_selector="#login-btn",
)
Formulario de registro
result = automator.submit_form(
url="https://example.com/register",
fields=[
("#first-name", "Jane"),
("#last-name", "Smith"),
("#email", "jane@example.com"),
("#password", "SecurePass!123"),
("#confirm-password", "SecurePass!123"),
],
submit_selector="#register-btn",
)
Formulario de búsqueda con CAPTCHA
result = automator.submit_form(
url="https://example.com/search",
fields=[
("#query", "python developer"),
("#location", "San Francisco"),
],
submit_selector="#search-btn",
)
Diagnóstico cuando el envío falla
Cuando un envío no pasa, casi siempre es por una de estas causas. Empieza por el token: resolverlo demasiado pronto es el fallo más frecuente, porque expira antes de llegar al servidor.
| Problema | Causa | Solución |
|---|---|---|
| Token rechazado | El token expiró antes del envío | Resuelve el CAPTCHA al final y envía de inmediato |
| Campo no encontrado | Carga de página dinámica | Añade esperas explícitas |
| Se detectó un tipo de CAPTCHA incorrecto | Varios elementos CAPTCHA en la página | Revisa el orden de detección |
| El formulario se recarga tras enviarlo | Falló la validación del lado del servidor | Verifica todos los campos obligatorios |
| El callback de reCAPTCHA no se dispara | Hay que invocar la función de callback | Usa grecaptcha.execute() tras la inyección |
Preguntas frecuentes
¿Qué plan de CaptchaAI necesito para automatizar formularios?
Depende de tu concurrencia, no del número de envíos. CaptchaAI factura por thread (CAPTCHA en curso), con resoluciones ilimitadas dentro del plan. Para un flujo de QA que valida formularios de uno en uno, el plan BASIC ($15/mes, 5 threads) suele bastar; si lanzas envíos en paralelo desde varios workers, sube a STANDARD ($30/mes, 15 threads) o superior. El coste mensual fijo en USD es predecible frente al pago por resolución.
¿Puedo resolver el CAPTCHA sin abrir un navegador?
Sí, para reCAPTCHA v2 y Turnstile puedes resolver el CAPTCHA por HTTP y enviar el formulario con un POST directo, sin Selenium. Ahora bien, si el formulario depende de validación en JavaScript o de un callback, necesitas un navegador real para que la página acepte el token.
¿Funciona con reCAPTCHA v3 o con hCaptcha?
CaptchaAI resuelve reCAPTCHA v2 y v3, Cloudflare Turnstile y CAPTCHA de imagen, así que puedes ampliar el detector para cubrir v3. hCaptcha no es compatible por ahora, de modo que no lo incluyas como tipo objetivo en tu automatización.
¿Por qué me rechazan el token justo al enviar?
Lo más habitual es que lo estés resolviendo demasiado pronto o escribiéndolo en el campo equivocado. Resuelve el CAPTCHA como último paso antes de pulsar enviar, y confirma que el token va a g-recaptcha-response para reCAPTCHA o a cf-turnstile-response para Turnstile.
Guías relacionadas
- Guía de inicio rápido de CaptchaAI
- Formatos de respuesta de la API
Automatiza cualquier formulario. Resuelve el CAPTCHA con CaptchaAI.