Casos de Uso

CAPTCHA Scraping con Node.js: Tutorial completo

¿Necesitas extraer datos de un sitio que te muestra un CAPTCHA justo antes de devolver el HTML? La respuesta corta es que no lo resuelves dentro de tu proceso: lo delegas. Tu script de Node.js detecta el sitekey en la página, se lo envía a la API de CaptchaAI, recibe un token válido y lo reenvía junto con el formulario. Todo el reconocimiento pesado ocurre fuera de tu scraper, así que este sigue haciendo lo que Node.js hace mejor: muchas solicitudes HTTP en paralelo sin bloquear el hilo. En esta guía montamos ese flujo de principio a fin con axios y cheerio.

El escenario típico en la región es el monitoreo de tu propio catálogo o de precios de referencia en un marketplace tipo MercadoLibre o Amazon.es: ciertas rutas de búsqueda empiezan a devolver un reCAPTCHA o un Cloudflare Turnstile después de varias solicitudes seguidas. Trabaja siempre sobre flujos que estés autorizado a automatizar y respeta los términos de servicio y la normativa de protección de datos aplicable.

Qué necesitas antes de empezar

El stack es deliberadamente ligero: dos dependencias y una clave. axios se encarga del transporte HTTP y cheerio te da un DOM al estilo jQuery para leer el HTML sin abrir un navegador.

Requisito Detalles
Node.js 16+ con npm
axios npm install axios
cheerio npm install cheerio
Clave API de CaptchaAI Desde captchaai.com

El módulo de resolución de CAPTCHA

Antes que nada conviene aislar la lógica de resolución en su propio módulo. El patrón es de dos pasos: _submit envía la tarea al endpoint in.php y devuelve un identificador; _poll consulta res.php cada cinco segundos hasta que el resultado está listo o se agota el tiempo de espera. Encima construimos métodos concretos para cada tipo: solveRecaptchaV2, solveRecaptchaV3 y solveTurnstile. Así el resto del scraper nunca toca los detalles de la API.

// captcha-solver.js
const axios = require("axios");

class CaptchaSolver {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = "https://ocr.captchaai.com";
  }

  async _submit(params) {
    params.key = this.apiKey;
    const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
    if (!resp.data.startsWith("OK|")) {
      throw new Error(`Submit error: ${resp.data}`);
    }
    return resp.data.split("|")[1];
  }

  async _poll(taskId, timeout = 300000) {
    const deadline = Date.now() + timeout;
    while (Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, 5000));
      const resp = await axios.get(`${this.baseUrl}/res.php`, {
        params: { key: this.apiKey, action: "get", id: taskId },
      });
      if (resp.data === "CAPCHA_NOT_READY") continue;
      if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
      throw new Error(`Solve error: ${resp.data}`);
    }
    throw new Error("Solve timed out");
  }

  async solveRecaptchaV2(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }

  async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
    const taskId = await this._submit({
      method: "userrecaptcha",
      googlekey: siteKey,
      pageurl: pageUrl,
      version: "v3",
      action,
    });
    return this._poll(taskId);
  }

  async solveTurnstile(siteKey, pageUrl) {
    const taskId = await this._submit({
      method: "turnstile",
      sitekey: siteKey,
      pageurl: pageUrl,
    });
    return this._poll(taskId);
  }
}

module.exports = CaptchaSolver;

Scraping de una página protegida con reCAPTCHA

El flujo completo de una página protegida se resume en cuatro pasos: cargas el HTML, buscas el sitekey, lo resuelves y reenvías el formulario con el token. cheerio localiza el atributo data-sitekey del div.g-recaptcha sin necesidad de renderizar JavaScript. Si no hay sitekey, la página no estaba protegida y ya tienes el HTML. Si lo hay, el token que devuelve CaptchaAI viaja en el campo g-recaptcha-response del POST.

const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");

const solver = new CaptchaSolver("YOUR_API_KEY");

async function scrapeProtectedPage(url) {
  // Step 1: Load the page
  const { data: html } = await axios.get(url, {
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  });

  const $ = cheerio.load(html);

  // Step 2: Extract site key
  const siteKey = $(".g-recaptcha").attr("data-sitekey");
  if (!siteKey) {
    console.log("No CAPTCHA found, page loaded directly");
    return html;
  }

  console.log("Site key found:", siteKey);

  // Step 3: Solve the CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);
  console.log("Token received:", token.substring(0, 50));

  // Step 4: Submit with the token
  const result = await axios.post(
    url,
    new URLSearchParams({
      "g-recaptcha-response": token,
      q: "search query",
    }),
    {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
      },
    }
  );

  return result.data;
}

Scraping de varias páginas en paralelo

Aquí es donde Node.js brilla. En lugar de resolver un CAPTCHA y esperar, lanzas un pequeño grupo de workers que consumen una cola de URLs y trabajan de forma concurrente. El parámetro concurrency marca cuántas resoluciones tienes en vuelo a la vez, y ese número debería alinearse con tus threads de CaptchaAI: cada thread es un CAPTCHA en curso, así que el plan BASIC ($15/mes, 5 threads) sostiene con holgura una concurrencia de 3, mientras que el plan ADVANCE ($90/mes, 50 threads) da margen para lotes mucho más grandes. Modera la concurrencia también según el límite de solicitudes del sitio de destino para no provocar bloqueos.

async function scrapePages(urls, siteKey, concurrency = 3) {
  const results = [];
  const queue = [...urls];

  const worker = async () => {
    while (queue.length > 0) {
      const url = queue.shift();
      try {
        const token = await solver.solveRecaptchaV2(siteKey, url);
        const { data } = await axios.post(
          url,
          new URLSearchParams({ "g-recaptcha-response": token }),
          {
            headers: {
              "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            },
          }
        );
        results.push({ url, data, success: true });
        console.log(`Scraped: ${url}`);
      } catch (err) {
        results.push({ url, error: err.message, success: false });
        console.error(`Failed: ${url} - ${err.message}`);
      }
    }
  };

  // Run workers concurrently
  const workers = Array(concurrency)
    .fill(null)
    .map(() => worker());
  await Promise.all(workers);

  return results;
}

// Usage
const urls = [
  "https://example.com/page/1",
  "https://example.com/page/2",
  "https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);

Cookies y sesiones persistentes

Muchos sitios validan el CAPTCHA una vez y luego confían en una cookie de sesión para las siguientes solicitudes. Si tu POST llega sin esa cookie, recibirás un 403 aunque el token sea correcto. La solución es envolver axios con un cookie jar: la carga inicial fija las cookies, tú resuelves el CAPTCHA y el mismo cliente reenvía todo con la sesión intacta.

const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");

const jar = new CookieJar();
const client = wrapper(
  axios.create({
    jar,
    headers: {
      "User-Agent":
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    },
  })
);

async function scrapeWithSession(url, siteKey) {
  // Initial page load sets cookies
  await client.get(url);

  // Solve CAPTCHA
  const token = await solver.solveRecaptchaV2(siteKey, url);

  // Submit with maintained cookies
  const result = await client.post(
    url,
    new URLSearchParams({ "g-recaptcha-response": token })
  );

  return result.data;
}

Extraer los resultados con Cheerio

Una vez superado el CAPTCHA, la parte de extracción es rutinaria. cheerio recorre el HTML con selectores CSS y te devuelve objetos limpios listos para guardar o exportar. Ajusta los selectores .result-item, .title y .description a la estructura real de tu página.

function parseResults(html) {
  const $ = cheerio.load(html);
  const items = [];

  $(".result-item").each((_, el) => {
    items.push({
      title: $(el).find(".title").text().trim(),
      url: $(el).find("a").attr("href"),
      description: $(el).find(".description").text().trim(),
    });
  });

  return items;
}

Errores frecuentes y cómo solucionarlos

Cuando el flujo falla, casi siempre es por una de estas cuatro causas. Esta tabla te ahorra el diagnóstico:

Problema Causa Solución
CAPTCHA_NOT_READY se repite sin fin sitekey incorrecto o resolución lenta Verifica el sitekey; aumenta el tiempo de espera
403 Forbidden en el POST Faltan cookies o encabezados Usa cookies de sesión; añade el encabezado Referer
Cheerio no encuentra los elementos Contenido dinámico por JavaScript Usa Puppeteer para sitios renderizados en JS
ECONNREFUSED El sitio de destino aplicó un límite de solicitudes Añade retrasos; rota proxies

Preguntas frecuentes

¿Cómo extraigo el sitekey si la página lo inyecta con JavaScript?

Cuando el div.g-recaptcha no está en el HTML inicial, cheerio no lo verá porque no ejecuta JavaScript. En ese caso busca el sitekey en el código fuente (suele aparecer en una llamada a grecaptcha.render o en un parámetro k= de un script de Google) o pásate a Puppeteer para leer el DOM ya renderizado y luego entrégaselo a solver.solveRecaptchaV2().

¿Cuántos threads necesito para scraping a gran escala?

Depende de tu concurrencia real. Como cada thread resuelve un CAPTCHA a la vez, tu techo de resoluciones simultáneas es igual al número de threads del plan. Para un scraper pequeño, BASIC ($15/mes, 5 threads) o STANDARD ($30/mes, 15 threads) bastan; si procesas decenas de miles de páginas por hora, ADVANCE ($90/mes, 50 threads) te da mucho más margen. Todos los planes incluyen resoluciones ilimitadas por thread.

El token es válido pero el POST devuelve 403, ¿qué reviso?

Casi siempre falta contexto de sesión. Confirma que envías las cookies de la carga inicial, que reutilizas el mismo User-Agent en todas las peticiones y que incluyes un encabezado Referer apuntando a la página de origen. El patrón con cookie jar de la sección de sesiones resuelve la mayoría de estos casos.

¿CaptchaAI resuelve el CAPTCHA de Cloudflare dentro de un scraper?

Sí, con un matiz. Si la página protege un formulario con Cloudflare Turnstile, llama a solver.solveTurnstile() igual que con reCAPTCHA. Si en cambio aparece la página completa de desafío de Cloudflare antes de cargar el sitio, usa el flujo de resolución del Cloudflare Challenge, que devuelve las cookies de validación que luego reenvías en tus solicitudes.

Guías relacionadas

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