Casos de Uso

Manejo de CAPTCHA para la automatización de la búsqueda de WHOIS de dominios

¿Automatizas la consulta de datos WHOIS y el proceso se detiene a los tres o cuatro dominios? El culpable casi siempre es un reCAPTCHA v2. Los portales WHOIS —desde el buscador de ICANN hasta las páginas de cada registrador— muestran un desafío CAPTCHA en cuanto detectan varias consultas seguidas desde la misma IP. La salida no es bajar tanto el ritmo que el script deje de ser útil, sino resolver ese reCAPTCHA v2 de forma programática con la API de CaptchaAI y seguir consultando sin intervención manual.

En esta guía montamos tres piezas: una clase de búsqueda WHOIS en Python que resuelve el CAPTCHA cuando aparece, un monitor de vencimientos en JavaScript y varias tácticas para que el desafío se dispare lo menos posible. Todo el código usa los endpoints in.php y res.php.

Por qué los portales WHOIS muestran CAPTCHA

No todos los portales reaccionan igual. Cada uno aplica su propio umbral, y conocerlos te dice cuándo esperar el desafío y cómo repartir la carga entre varias fuentes antes de que salte.

Tipo de portal CAPTCHA Umbral de activación
WHOIS de ICANN reCAPTCHA v2 3-5 consultas por sesión
Páginas de búsqueda de registradores reCAPTCHA v2/v3 5-10 consultas por minuto
NIR Regional (APNIC, RIPE) CAPTCHA de imagen 10-20 consultas
WHOIS de subastas de dominios Cloudflare Turnstile Comprobaciones rápidas de dominio
Herramientas WHOIS masivas CAPTCHA personalizado Tras el límite del nivel gratuito

La mayoría de los desafíos son reCAPTCHA v2, así que ese es el caso que resolvemos primero. Los portales de subastas suelen usar Cloudflare Turnstile; CaptchaAI resuelve ambos con el mismo flujo de envío y sondeo, solo cambia el method.

Resolver el reCAPTCHA v2 de la búsqueda WHOIS en Python

La clase siguiente encapsula el patrón completo: hace la consulta, detecta si el HTML trae un reCAPTCHA v2, extrae el sitekey, lo manda a CaptchaAI, espera el token y reenvía el formulario con g-recaptcha-response. El método bulk_lookup recorre una lista de dominios con un retraso configurable, y check_availability separa los que están libres de los ya registrados.

import requests
import time
import re

class WhoisLookup:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def lookup(self, domain, whois_url):
        """Look up WHOIS data for a domain, solving CAPTCHAs as needed."""
        response = self.session.get(whois_url, params={"domain": domain})

        if self._has_recaptcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(site_key, whois_url)
            response = self.session.post(whois_url, data={
                "domain": domain,
                "g-recaptcha-response": token
            })

        return self._parse_whois(response.text)

    def bulk_lookup(self, domains, whois_url, delay=3):
        """Look up WHOIS for multiple domains."""
        results = {}
        for domain in domains:
            try:
                results[domain] = self.lookup(domain, whois_url)
            except Exception as e:
                results[domain] = {"error": str(e)}
            time.sleep(delay)
        return results

    def check_availability(self, domains, whois_url):
        """Check which domains are available for registration."""
        results = self.bulk_lookup(domains, whois_url)
        available = []
        taken = []

        for domain, data in results.items():
            if data.get("error") or data.get("status") == "available":
                available.append(domain)
            else:
                taken.append(domain)

        return {"available": available, "taken": taken}

    def _has_recaptcha(self, html):
        return "g-recaptcha" in html or "recaptcha" in html.lower()

    def _extract_site_key(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            return match.group(1)
        raise ValueError("reCAPTCHA site key not found")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": page_url,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(60):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("reCAPTCHA solve timed out")

    def _parse_whois(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        # Look for WHOIS data in pre-formatted blocks or tables
        raw_whois = soup.select_one("pre, .whois-data, #whois-result")
        if raw_whois:
            text = raw_whois.get_text()
            return self._extract_fields(text)

        return {"raw": soup.get_text()[:2000]}

    def _extract_fields(self, text):
        fields = {}
        patterns = {
            "registrar": r"Registrar:\s*(.+)",
            "created": r"Creat(?:ed|ion) Date:\s*(.+)",
            "expires": r"(?:Expir(?:y|ation)|Registry Expiry) Date:\s*(.+)",
            "updated": r"Updated Date:\s*(.+)",
            "status": r"(?:Domain )?Status:\s*(.+)",
            "nameservers": r"Name Server:\s*(.+)",
            "registrant": r"Registrant (?:Name|Organization):\s*(.+)"
        }

        for field, pattern in patterns.items():
            matches = re.findall(pattern, text, re.IGNORECASE)
            if matches:
                fields[field] = matches if len(matches) > 1 else matches[0].strip()

        return fields


# Usage
whois = WhoisLookup("YOUR_API_KEY")

# Single lookup
result = whois.lookup("example.com", "https://whois.example.com/lookup")
print(f"Registrar: {result.get('registrar')}")
print(f"Expires: {result.get('expires')}")

# Bulk availability check
domains = ["startup-name.com", "my-project.io", "cool-app.dev"]
availability = whois.check_availability(domains, "https://whois.example.com/lookup")
print(f"Available: {availability['available']}")

Un detalle que evita la mitad de los errores: el token de reCAPTCHA v2 caduca pronto, así que resuelve y reenvía dentro de la misma sesión, sin demoras largas entre ambos pasos.

Monitorear el vencimiento de dominios en JavaScript

Si lo tuyo no es una consulta puntual sino vigilar una cartera de dominios propios o de clientes, este monitor en JavaScript mantiene una lista de observación y avisa cuando a alguno le quedan 30 días o menos. La lógica de resolución es idéntica —detecta el g-recaptcha, resuelve con CaptchaAI y reenvía— pero orientada a ejecutarse de forma periódica.

class DomainMonitor {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.watchList = new Map();
  }

  addDomain(domain, whoisUrl) {
    this.watchList.set(domain, { url: whoisUrl, history: [] });
  }

  async checkExpirations() {
    const expiring = [];

    for (const [domain, config] of this.watchList) {
      try {
        const data = await this.lookup(domain, config.url);
        config.history.push({ ...data, checkedAt: new Date().toISOString() });

        if (data.expires) {
          const daysLeft = Math.ceil(
            (new Date(data.expires) - new Date()) / (1000 * 60 * 60 * 24)
          );
          if (daysLeft <= 30) {
            expiring.push({ domain, daysLeft, expires: data.expires });
          }
        }
      } catch (error) {
        console.error(`Failed to check ${domain}: ${error.message}`);
      }
    }

    return expiring;
  }

  async lookup(domain, whoisUrl) {
    const response = await fetch(`${whoisUrl}?domain=${domain}`);
    const html = await response.text();

    if (html.includes('g-recaptcha')) {
      return this.solveAndLookup(domain, whoisUrl, html);
    }

    return this.parseWhois(html);
  }

  async solveAndLookup(domain, whoisUrl, html) {
    const match = html.match(/data-sitekey="([^"]+)"/);
    if (!match) throw new Error('No reCAPTCHA site key found');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'userrecaptcha',
        googlekey: match[1],
        pageurl: whoisUrl,
        json: '1'
      })
    });
    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) {
        const response = await fetch(whoisUrl, {
          method: 'POST',
          body: new URLSearchParams({
            domain,
            'g-recaptcha-response': data.request
          })
        });
        return this.parseWhois(await response.text());
      }
    }
    throw new Error('reCAPTCHA solve timed out');
  }

  parseWhois(html) {
    const extract = (pattern) => {
      const match = html.match(pattern);
      return match ? match[1].trim() : null;
    };

    return {
      registrar: extract(/Registrar:\s*([^\n<]+)/i),
      created: extract(/Creat(?:ed|ion) Date:\s*([^\n<]+)/i),
      expires: extract(/(?:Expir(?:y|ation)|Registry Expiry) Date:\s*([^\n<]+)/i),
      status: extract(/(?:Domain )?Status:\s*([^\n<]+)/i)
    };
  }
}

// Usage
const monitor = new DomainMonitor('YOUR_API_KEY');
monitor.addDomain('example.com', 'https://whois.example.com/lookup');
monitor.addDomain('mysite.io', 'https://whois.example.com/lookup');

const expiring = await monitor.checkExpirations();
expiring.forEach(d => console.log(`${d.domain} expires in ${d.daysLeft} days`));

Programa checkExpirations() con un cron diario o dentro de una función serverless y guarda el historial en tu propia base de datos para tener trazabilidad de cada comprobación.

Cómo reducir la aparición de CAPTCHA

Resolver el desafío es barato, pero evitarlo cuando no hace falta es más rápido todavía. Estas cuatro tácticas bajan la frecuencia con la que el portal decide mostrarte un reCAPTCHA v2.

Estrategia Beneficio
Caché de resultados localmente Evita búsquedas repetidas para el mismo dominio
Usa retrasos de 3 a 5 segundos Reduce la tasa de activación de CAPTCHA
Rota entre portales WHOIS Distribuye la carga entre proveedores
Persistencia de sesión Mantiene el estado de autorización CAPTCHA

La combinación que mejor funciona es caché más rotación de fuentes: si ya consultaste un dominio esta semana, léelo de tu almacén local y reparte lo nuevo entre dos o tres buscadores distintos.

Errores frecuentes y cómo resolverlos

Problema Causa Solución
CAPTCHA tras 3 consultas Límite de tasa del portal Aumenta el retraso, usa proxies
WHOIS devuelve "No hay coincidencias" Privacidad/RDAP redactado Prueba un portal WHOIS alternativo
Token reCAPTCHA rechazado El token expiró antes de enviarlo Resuelve y envía en menos de 2 minutos
IP bloqueada Se superó el límite de consultas diarias Rota proxies residenciales

Un caso práctico: la cartera de dominios de una agencia

Imagina una agencia en Ciudad de México que gestiona los dominios de varios clientes: unos cuantos .mx, algún .es y una lista de .com de marca que hay que renovar a tiempo. A mano, revisar cien dominios en el WHOIS de los registradores es media mañana perdida, y el reCAPTCHA v2 corta el proceso cada pocas consultas. Con el monitor en JavaScript corriendo una vez al día, la agencia recibe por la mañana la lista de dominios que vencen en menos de 30 días, con el CAPTCHA resuelto de forma transparente.

En cuanto al costo, CaptchaAI cobra por thread concurrente y no por resolución, así que el gasto es predecible en USD: el plan BASIC ($15/mes, 5 threads) cubre de sobra una cartera pequeña por lotes, y para verificaciones masivas de disponibilidad puedes escalar a ADVANCE ($90/mes, 50 threads) sin cambiar una línea de código. Para agencias y freelancers que facturan en monedas locales volátiles, ese costo mensual fijo en dólares es más fácil de presupuestar. Como siempre en tareas de extracción de datos, respeta los términos de servicio del portal y la normativa de protección de datos aplicable.

Preguntas frecuentes

¿CaptchaAI resuelve el reCAPTCHA v2 de los portales WHOIS?

Sí. El flujo es el que ves en el código: envías el sitekey y la URL de la página a in.php, sondeas res.php hasta recibir el token y lo devuelves al formulario en g-recaptcha-response. El mismo patrón sirve para el Cloudflare Turnstile de los portales de subastas, cambiando solo el method.

¿Qué plan de CaptchaAI necesito para búsquedas WHOIS masivas?

Depende de cuántas consultas quieras tener en vuelo a la vez, no del total diario, porque cada plan incluye resoluciones ilimitadas por thread. Para lotes moderados el plan BASIC ($15/mes, 5 threads) es suficiente; si lanzas comprobaciones muy paralelas, ADVANCE ($90/mes, 50 threads) da más margen. Consulta los precios vigentes en captchaai.com/pricing.

¿Por qué el portal rechaza el token de reCAPTCHA al enviar la consulta?

Casi siempre porque el token caducó. Un reCAPTCHA v2 tiene una vida corta, así que resuelve y reenvía dentro de la misma sesión y en menos de dos minutos. Si sigue fallando, comprueba que extraes el sitekey correcto y que envías la misma pageurl que vio el navegador.

¿Conviene el protocolo WHOIS por el puerto 43 en lugar de los portales web?

El puerto 43 no muestra CAPTCHA, pero aplica límites de solicitudes estrictos y devuelve datos recortados por el RGPD. Los portales web suelen enseñar más campos detrás del reCAPTCHA v2, por lo que para datos completos compensa automatizar el portal con CaptchaAI.

Artículos relacionados

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