Casos de Uso

CAPTCHA scraping con Python sin abrir un navegador

¿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.
  • requestspip install requests.
  • beautifulsoup4pip 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.

Guías relacionadas

Los comentarios están deshabilitados para este artículo.