Casi todos los equipos mandan el CAPTCHA de imagen desnudo: solo píxeles, ninguna pista de lo que contienen. Un CAPTCHA de varios caracteres se resuelve mejor cuando le dices al solver qué esperar — longitud aproximada, si distingue mayúsculas, si hay líneas de ruido que debe ignorar — y solo retocas la imagen cuando esos hints ya no alcanzan. Esta guía recorre ese orden de trabajo con la API de CaptchaAI: primero clasificar la dificultad, después enviar con los parámetros adecuados y dejar el preprocesamiento de píxeles como último recurso.
Clasifica primero el tipo de dificultad
Antes de tocar código conviene saber contra qué estás. Estas son las variantes que aparecen una y otra vez en portales públicos y paneles de administración antiguos:
| Tipo | Descripción | Dificultad |
|---|---|---|
| Texto limpio | Sin distorsión, fuente uniforme | Fácil |
| Texto deformado | Letras rotadas o escaladas una a una | Media |
| Letras pegadas | Los caracteres se tocan o se superponen | Alta |
| Varias fuentes | Una tipografía distinta por carácter | Alta |
| Ruido y líneas | Fondo con manchas, líneas que tachan el texto | Media |
| Variación de color | Un color diferente por carácter | Media |
| Expresión matemática | Números y operadores; se espera el resultado | Media |
La clasificación no es un ejercicio académico: determina qué hints envías y si el preprocesamiento te va a ayudar o a estropear la imagen.
Diagnóstico rápido de los fallos frecuentes
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Faltan caracteres | Letras pegadas leídas como una sola | Añade textinstructions describiendo el texto conectado |
| Sobran caracteres | El ruido se interpreta como texto | Preprocesa para limpiar el fondo antes de enviar |
| Mayúsculas y minúsculas cambiadas | No se conserva el caso | Establece regsense=1 |
| Devuelve la expresión en vez del resultado | Falta calc=1 |
Activa el modo de cálculo para los CAPTCHA matemáticos |
| Siempre falla en un sitio concreto | Tipografía propia de ese sitio | Reporta las respuestas malas con reportbad |
Un caso real: monitorizar un portal de trámites
Un escenario habitual en el mercado hispanohablante: un equipo pequeño monitoriza un portal de trámites públicos — el tipo de sitio de cita previa o de consulta fiscal que sigue sirviendo CAPTCHA de texto renderizado en el servidor. La imagen mide 160x60 px, mezcla mayúsculas y minúsculas y lleva dos líneas diagonales sobre el texto:
- Con el envío por defecto, los aciertos se quedan cortos.
- Al añadir
regsense=1y una instrucción explícita sobre las líneas, la misma imagen se resuelve de forma estable sin tocar un solo píxel. - Antes de automatizar el portal, revisa sus términos de servicio y la normativa de protección de datos aplicable: automatizar un flujo propio o autorizado no es lo mismo que extraer datos de terceros.
En cuanto al costo, CaptchaAI factura por thread concurrente y no por resolución: el plan BASIC ($15/mes, 5 threads) cubre de sobra un monitor de este tamaño, y STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) escalan cuando el mismo worker vigila varios portales. Los precios están en USD, lo que deja un costo mensual predecible frente al pago por cada resolución.
Envío base con hints
Todo lo demás se construye sobre esta función: codificas la imagen en base64, la envías a in.php con los hints que correspondan y consultas el resultado en res.php hasta que deje de responder CAPCHA_NOT_READY.
import requests
import base64
import time
import os
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def solve_complex_image(image_b64, hints=None):
"""Solve a complex multi-character image CAPTCHA."""
payload = {
"key": API_KEY,
"method": "base64",
"body": image_b64,
"json": 1,
}
if hints:
payload.update(hints)
resp = requests.post(
"https://ocr.captchaai.com/in.php",
data=payload,
timeout=30,
)
result = resp.json()
if result.get("status") != 1:
raise RuntimeError(f"Submit failed: {result.get('request')}")
task_id = result["request"]
time.sleep(8)
for _ in range(24):
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
return data["request"]
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(data["request"])
time.sleep(5)
raise TimeoutError("Solve timeout")
Dos detalles que se pasan por alto: la espera inicial de 8 segundos evita quemar consultas inútiles, y el bucle distingue entre "todavía no está listo" y un error real. Si tratas como fallo cualquier respuesta que no sea la solución, reintentarás tareas que iban a resolverse bien.
Letras pegadas: acota la longitud
Los caracteres que se tocan rompen al OCR clásico: la segmentación ya no encuentra dónde termina una letra y empieza la siguiente. El hint decisivo aquí es el rango de longitud: fijar minLen y maxLen reduce muchísimo el espacio de respuestas plausibles.
def solve_connected_letters(image_path):
"""Solve CAPTCHA with connected/overlapping characters."""
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return solve_complex_image(b64, hints={
"textinstructions": "Characters may be connected or overlapping",
"minLen": 4,
"maxLen": 8,
})
Ruido de fondo y mayúsculas mezcladas
Cuando el formulario distingue mayúsculas de minúsculas, regsense=1 deja de ser opcional: sin él recibirás la cadena correcta con el caso equivocado y el sitio la rechazará igual. Añade language=2 para acotar el alfabeto latino y una instrucción que diga qué debe ignorarse.
def solve_noisy_mixed(image_path):
"""Solve CAPTCHA with background noise and mixed case."""
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return solve_complex_image(b64, hints={
"regsense": 1, # Case-sensitive
"language": 2, # Latin characters
"textinstructions": "Ignore background lines and noise",
})
Una tipografía distinta por carácter
Las imágenes con varias fuentes despistan a los modelos que aprenden una sola forma por letra. Describir esa variación de forma explícita y ajustar el rango de longitud suele bastar:
def solve_multi_font(image_path):
"""Solve CAPTCHA using multiple fonts per character."""
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return solve_complex_image(b64, hints={
"textinstructions": "Each character may use a different font or style",
"minLen": 5,
"maxLen": 7,
})
Preprocesamiento: solo cuando los hints se quedan cortos
CaptchaAI maneja la mayoría de las distorsiones tal como llegan, así que preprocesar es una optimización, no un paso obligatorio. Tiene sentido en imágenes muy oscuras, con contraste bajo o con fondo texturizado. Sobre una imagen ya legible, la binarización puede borrar trazos finos y empeorar el resultado: mide antes y después con el mismo lote.
# preprocess.py
from PIL import Image, ImageFilter, ImageEnhance
import io
import base64
def preprocess_for_ocr(image_path):
"""Preprocess image to improve OCR accuracy."""
img = Image.open(image_path)
# Convert to grayscale
img = img.convert("L")
# Increase contrast
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(2.0)
# Sharpen
img = img.filter(ImageFilter.SHARPEN)
# Binarize (threshold)
threshold = 128
img = img.point(lambda p: 255 if p > threshold else 0)
# Encode back to base64
buffer = io.BytesIO()
img.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("ascii")
Reintentos en cascada y reporte de fallos
En producción, un único intento con hints estrictos es frágil: si el rango de longitud o el modo case-sensitive no encajan con esa imagen, pierdes el intento entero. Lo que aguanta es relajar las restricciones de forma progresiva y cerrar el ciclo reportando las respuestas incorrectas con reportbad.
# retry_strategy.py
def solve_with_retry(image_b64, hints, max_retries=3):
"""Retry solving with fallback strategies."""
strategies = [
hints, # Original hints
{**hints, "textinstructions": ""}, # Without instructions
{**hints, "numeric": 0, "regsense": 0}, # Relaxed constraints
]
for i, strategy in enumerate(strategies[:max_retries]):
try:
result = solve_complex_image(image_b64, strategy)
return {"text": result, "strategy": i, "success": True}
except RuntimeError:
continue
return {"text": None, "strategy": -1, "success": False}
def report_bad_answer(task_id):
"""Report incorrect answer for quality feedback."""
requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY,
"action": "reportbad",
"id": task_id,
}, timeout=10)
Guarda también el índice de la estrategia ganadora. Si en un dominio el grueso de los aciertos llega con la variante relajada, esa debería ser tu configuración por defecto allí, no la tercera opción de la cascada.
Preguntas frecuentes
¿Qué hint aporta más en imágenes de varios caracteres?
El rango de longitud. Fijar minLen y maxLen acota el espacio de respuestas más que cualquier instrucción de texto, sobre todo cuando las letras se tocan entre sí.
¿Una imagen difícil consume más saldo que una fácil?
No. CaptchaAI factura por thread concurrente y cada plan incluye resoluciones ilimitadas por thread, así que una imagen difícil no cuesta más: simplemente ocupa el thread durante más tiempo.
¿Puedo enviar la imagen por URL en lugar de base64?
El flujo de esta guía usa method=base64, que es el más fiable cuando la imagen viene detrás de una sesión autenticada o de una cookie. Descárgala con la misma sesión de tu scraper y codifícala antes de enviarla.
¿Sirve este método para reCAPTCHA o Cloudflare Turnstile?
No. Son familias distintas, con un flujo basado en sitekey en lugar de imagen. CaptchaAI resuelve reCAPTCHA v2 y v3, Cloudflare Turnstile y GeeTest v3, pero con métodos diferentes al de esta guía.
¿Cuántos caracteres admite un CAPTCHA de imagen?
CaptchaAI trabaja con cadenas de hasta unos 20 caracteres, aunque la gran mayoría de los CAPTCHA reales tienen entre 4 y 8.
Guías relacionadas
¿Tienes un lote de imágenes que se resiste al OCR? Empieza con CaptchaAI y prueba los hints de esta guía sobre tus propias capturas.