Respuesta corta: envía la cadena base64 sin el prefijo data:image/png;base64,, generada a partir de bytes leídos en modo binario ("rb"), preferiblemente en PNG y por debajo de 600 KB. Con esas cuatro condiciones desaparece la mayoría de los ERROR_ que devuelve in.php antes de que llegues siquiera a depurar.
Casi ningún fallo que se atribuye al motor de OCR es un fallo de resolución: son bytes rotos que salieron mal de tu script. El servicio recibe algo que no es una imagen y responde con un código genérico, así que el diagnóstico se hace en tu lado, no en el suyo. Esta guía recorre el camino completo en Python: el envío, de dónde sacar los bytes, qué rompe la codificación, cómo validar antes de gastar un thread y qué formato conviene.
Cómo se envía una imagen en base64 a CaptchaAI
CaptchaAI acepta CAPTCHA de imagen en base64 mediante el parámetro method=base64 contra el endpoint in.php:
import requests
import base64
import os
def submit_image_captcha(image_base64):
"""Submit base64-encoded image to CaptchaAI."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": os.environ["CAPTCHAAI_API_KEY"],
"method": "base64",
"body": image_base64,
"json": 1,
}, timeout=30)
return resp.json()
Tres detalles que conviene fijar. El campo body lleva solo la cadena base64. La clave API sale de una variable de entorno, nunca del código. Y json: 1 devuelve una respuesta estructurada, más cómoda que el formato de texto plano heredado.
El modelo de facturación juega a tu favor mientras depuras: CaptchaAI cobra por thread concurrente, desde BASIC ($15/mes, 5 threads) hasta VIP-3 ($7,500/mes, 5.000 threads), con resoluciones ilimitadas por thread. Un envío mal codificado no te cuesta un crédito suelto, pero ocupa un thread durante el tiempo de espera — por eso la validación local se paga sola.
Obtener los bytes: archivo, URL y captura de Selenium
Desde un archivo en disco
El caso base. Lo único que no se puede negociar es el modo binario:
# from_file.py
import base64
def encode_from_file(filepath):
"""Read an image file and return base64 string."""
with open(filepath, "rb") as f:
raw = f.read()
return base64.b64encode(raw).decode("ascii")
# Usage
b64 = encode_from_file("captcha.png")
print(f"Encoded length: {len(b64)} chars")
Descargando la imagen desde su URL
Muchos portales sirven el CAPTCHA como una imagen con su propia URL. Antes de codificar, comprueba el Content-Type: si la sesión caducó, lo que descargas suele ser un HTML de redirección y no un PNG.
# from_url.py
import requests
import base64
def encode_from_url(image_url):
"""Download image and return base64 string."""
resp = requests.get(image_url, timeout=15)
resp.raise_for_status()
# Verify it's actually an image
content_type = resp.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise ValueError(f"Not an image: {content_type}")
return base64.b64encode(resp.content).decode("ascii")
# Usage
b64 = encode_from_url("https://example.com/captcha.png")
Ojo: si el portal genera el CAPTCHA por sesión, descargarlo con un cliente HTTP distinto al del navegador te dará otra imagen. Ahí la captura del elemento es la vía correcta.
Capturando el elemento con Selenium
# from_selenium.py
import base64
from selenium.webdriver.common.by import By
def encode_from_element(driver, selector):
"""Screenshot a specific element and return base64."""
element = driver.find_element(By.CSS_SELECTOR, selector)
screenshot_b64 = element.screenshot_as_base64
return screenshot_b64
def encode_from_page_crop(driver, selector):
"""Crop a specific region from the page screenshot."""
from PIL import Image
import io
element = driver.find_element(By.CSS_SELECTOR, selector)
location = element.location
size = element.size
# Full page screenshot
png = driver.get_screenshot_as_png()
img = Image.open(io.BytesIO(png))
# Crop to element bounds
left = location["x"]
top = location["y"]
right = left + size["width"]
bottom = top + size["height"]
cropped = img.crop((left, top, right, bottom))
# Encode
buffer = io.BytesIO()
cropped.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode("ascii")
screenshot_as_base64 ya devuelve base64: úsalo tal cual. La segunda función sirve cuando el elemento no se puede capturar aislado — dentro de un canvas o de un contenedor con overflow — y hay que recortar la región de la captura completa.
Los tres errores de codificación que rompen el envío
Error 1: dejar el prefijo del data URI
Si extraes el src de un <img> incrustado, viene con prefijo. El servicio espera la carga útil, no la etiqueta:
# WRONG — includes data URI prefix
bad = "data:image/png;base64,iVBORw0KGgo..."
# RIGHT — raw base64 only
good = "iVBORw0KGgo..."
# Fix: Strip the prefix
def clean_base64(b64_string):
if "," in b64_string:
return b64_string.split(",", 1)[1]
return b64_string
Error 2: codificar dos veces
# WRONG — encoding an already-encoded string
already_b64 = element.screenshot_as_base64
double_encoded = base64.b64encode(already_b64.encode()).decode() # BAD
# RIGHT — use as-is
correct = element.screenshot_as_base64 # Already base64
Error 3: codificar texto en lugar de bytes
Abrir la imagen en modo texto corrompe los bytes de forma silenciosa en cuanto aparece un carácter no decodificable. El archivo "se lee", el script no lanza ninguna excepción y el envío falla:
# WRONG — reading as text
with open("captcha.png", "r") as f: # Text mode
content = f.read() # Corrupted binary data
# RIGHT — reading as bytes
with open("captcha.png", "rb") as f: # Binary mode
content = f.read()
encoded = base64.b64encode(content).decode("ascii")
Valida antes de enviar y ahorra un ciclo de sondeo
Enviar una imagen inválida cuesta un envío, un tiempo de espera y una consulta a res.php para descubrir algo que se detecta localmente en milisegundos. Esta función revisa prefijo, decodificación, tamaño y firma binaria del formato:
# validate.py
import base64
import io
def validate_captcha_image(b64_string):
"""Validate base64 image before submitting to CaptchaAI."""
errors = []
# Check for data URI prefix
if b64_string.startswith("data:"):
errors.append("Contains data URI prefix — strip it")
b64_string = b64_string.split(",", 1)[1]
# Try decoding
try:
decoded = base64.b64decode(b64_string)
except Exception as e:
return {"valid": False, "errors": [f"Invalid base64: {e}"]}
# Check size
size_kb = len(decoded) / 1024
if size_kb < 1:
errors.append(f"Image too small ({size_kb:.1f} KB) — likely corrupt")
if size_kb > 500:
errors.append(f"Image large ({size_kb:.1f} KB) — consider resizing")
# Check image format
if decoded[:8] == b'\x89PNG\r\n\x1a\n':
fmt = "PNG"
elif decoded[:3] == b'\xff\xd8\xff':
fmt = "JPEG"
elif decoded[:4] == b'GIF8':
fmt = "GIF"
elif decoded[:4] == b'RIFF':
fmt = "WEBP"
else:
errors.append("Unknown image format")
fmt = "unknown"
return {
"valid": len(errors) == 0,
"format": fmt,
"size_kb": round(size_kb, 1),
"errors": errors,
}
# Usage
result = validate_captcha_image(b64_string)
if not result["valid"]:
print(f"Issues: {result['errors']}")
else:
print(f"Valid {result['format']}, {result['size_kb']} KB")
Colócala como guardia donde encolas la tarea. En un scraper de miles de páginas al día, filtrar las imágenes corruptas mantiene tus threads en trabajo útil y no en esperas perdidas.
Qué formato elegir según el desafío
| Formato | Mejor para | Tamaño | Calidad |
|---|---|---|---|
| PNG | CAPTCHA de texto, capturas de pantalla | Más grande | Sin pérdidas |
| JPEG | CAPTCHA basados en fotografías | Más pequeño | Con pérdida (usa calidad ≥ 85) |
| GIF | CAPTCHA animados | variable | Colores limitados |
| WEBP | Navegadores modernos | Más pequeño | Buena calidad |
Recomendación: usa PNG para los CAPTCHA de texto. La compresión sin pérdidas conserva los bordes de los caracteres, y ese contorno limpio es lo que necesita el OCR para distinguir una l de un 1. El JPEG agresivo introduce artefactos en los trazos finos: es la causa más frecuente de una lectura casi correcta con un carácter cambiado.
Un caso típico: portales públicos con CAPTCHA de imagen
Piensa en un equipo que monitoriza un trámite en un portal público — una cita previa administrativa en España, un trámite del SAT en México — o que verifica su propio catálogo en un marketplace regional. Son páginas antiguas que sirven un GIF o un PNG pequeño, de 3 a 8 KB, con texto distorsionado: ahí no hace falta comprimir ni redimensionar, y cualquier reescalado "por si acaso" empeora la lectura. Redimensiona solo cuando partas de una captura de página completa, que sí puede acercarse a los 600 KB. Y en cualquier automatización de este tipo: respeta los términos de servicio del sitio y la normativa de protección de datos aplicable.
Solución de problemas
| Problema | Causa | Solución |
|---|---|---|
ERROR_WRONG_FILE_EXTENSION |
Datos base64 no válidos | Validar con validate_captcha_image() |
ERROR_TOO_BIG_CAPTCHA_FILESIZE |
Imagen de más de 600 KB | Redimensionar o comprimir antes de codificar |
ERROR_ZERO_CAPTCHA_FILESIZE |
Imagen vacía o corrupta | Comprobar que la descarga terminó bien |
| Resultado de resolución incorrecto | JPEG sobrecomprimido | Usar PNG o JPEG con calidad ≥ 85 |
Si el error se repite con la misma fuente de imágenes, guarda en disco el resultado de base64.b64decode() y ábrelo con un visor: sabrás en segundos si el problema está en la codificación o en lo que descargaste.
Preguntas frecuentes
¿Por qué falla mi base64 si la imagen se ve bien en el navegador?
Lo que ves en pantalla y lo que envía tu script rara vez son los mismos bytes. Las causas habituales: prefijo data: sin recortar, doble codificación de screenshot_as_base64, o una descarga que devolvió el HTML de una sesión caducada. Decodifica la cadena a disco y ábrela para salir de dudas.
¿Hace falta recortar la imagen a solo el CAPTCHA?
Sí, conviene. Una captura de página completa añade texto y elementos de interfaz que compiten con el desafío. La función encode_from_page_crop() recorta a los límites exactos del elemento.
¿Sirve este flujo para reCAPTCHA o Turnstile?
No. El envío en base64 es para CAPTCHA de imagen y OCR. reCAPTCHA v2/v3, Cloudflare Turnstile y GeeTest v3 usan métodos propios basados en sitekey y devuelven un token, no una cadena de texto.
¿Puedo enviar imágenes en SVG?
No. Conviértelo antes a PNG con una biblioteca como Pillow o cairosvg; el envío espera un mapa de bits, no un formato vectorial.
¿Cuánto puede pesar como máximo el cuerpo base64?
600 KB. Ten en cuenta que la codificación base64 añade aproximadamente un tercio al tamaño original, así que un PNG de 460 KB ya roza el límite una vez codificado.
Guías relacionadas
- cómo mejorar la precisión del OCR ajustando los parámetros
- preprocesar la imagen para subir la tasa de resolución
Codifica bien y deja el resto al OCR: empieza con CaptchaAI.