Tutoriais

Construindo uma fila de resolução CAPTCHA em Node.js

O Node.js é de thread único, mas é excelente na simultaneidade I/O — perfeito para resolver CAPTCHA onde você está aguardando respostas da API. Este guia cobre padrões de fila, desde o simples Promise.all até sistemas de trabalho de nível de produção.


Lote simples: Promise.allSettled

const API_KEY = "YOUR_API_KEY";

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function solveSingle(method, params) {
  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
  });
  const submitData = await submitResp.json();
  if (submitData.status !== 1) throw new Error(submitData.request);
  const taskId = submitData.request;

  for (let i = 0; i < 30; i++) {
    await sleep(5000);
    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const data = await pollResp.json();
    if (data.status === 1) return data.request;
    if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
  }
  throw new Error("Timed out");
}

// Solve all at once
async function solveBatch(tasks) {
  const results = await Promise.allSettled(
    tasks.map((task) => solveSingle(task.method, task.params))
  );

  return results.map((result, i) => ({
    taskId: tasks[i].id,
    status: result.status,
    value: result.status === "fulfilled" ? result.value : null,
    error: result.status === "rejected" ? result.reason.message : null,
  }));
}

// Usage
const tasks = Array.from({ length: 10 }, (_, i) => ({
  id: i,
  method: "userrecaptcha",
  params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));

const results = await solveBatch(tasks);
console.log(`Solved: ${results.filter((r) => r.status === "fulfilled").length}/10`);

Fila limitada por simultaneidade

Controle quantas soluções CAPTCHA são executadas em paralelo:

class ConcurrencyQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
    this.results = [];
  }

  add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.#process();
    });
  }

  async #process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) return;

    this.running++;
    const { fn, resolve, reject } = this.queue.shift();

    try {
      const result = await fn();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.#process();
    }
  }

  async addBatch(fns) {
    return Promise.allSettled(fns.map((fn) => this.add(fn)));
  }
}

// Usage
const queue = new ConcurrencyQueue(5);

const tasks = Array.from({ length: 20 }, (_, i) => () =>
  solveSingle("userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  })
);

const results = await queue.addBatch(tasks);
const solved = results.filter((r) => r.status === "fulfilled");
console.log(`Solved: ${solved.length}/${results.length}`);

Fila baseada em EventEmitter

Para acompanhamento do progresso em tempo real:

const { EventEmitter } = require("events");

class CaptchaQueue extends EventEmitter {
  #apiKey;
  #maxConcurrent;
  #pending;
  #active;

  constructor(apiKey, maxConcurrent = 5) {
    super();
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#pending = [];
    this.#active = 0;
    this.stats = { submitted: 0, solved: 0, failed: 0 };
  }

  submit(id, method, params) {
    this.#pending.push({ id, method, params });
    this.stats.submitted++;
    this.emit("submitted", { id, total: this.stats.submitted });
    this.#drain();
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#pending.length > 0) {
      const task = this.#pending.shift();
      this.#active++;
      this.#solve(task).finally(() => {
        this.#active--;
        this.#drain();
        if (this.#active === 0 && this.#pending.length === 0) {
          this.emit("complete", this.stats);
        }
      });
    }
  }

  async #solve(task) {
    try {
      const token = await solveSingle(task.method, task.params);
      this.stats.solved++;
      this.emit("solved", { id: task.id, token, stats: { ...this.stats } });
    } catch (error) {
      this.stats.failed++;
      this.emit("failed", { id: task.id, error: error.message, stats: { ...this.stats } });
    }
  }
}

// Usage
const queue = new CaptchaQueue("YOUR_API_KEY", 5);

queue.on("submitted", ({ id, total }) => {
  console.log(`Submitted #${id} (total: ${total})`);
});

queue.on("solved", ({ id, stats }) => {
  console.log(`Solved #${id} — ${stats.solved}/${stats.submitted}`);
});

queue.on("failed", ({ id, error }) => {
  console.log(`Failed #${id}: ${error}`);
});

queue.on("complete", (stats) => {
  const rate = ((stats.solved / stats.submitted) * 100).toFixed(1);
  console.log(`Done: ${stats.solved}/${stats.submitted} (${rate}%)`);
});

// Submit tasks
for (let i = 0; i < 15; i++) {
  queue.submit(i, "userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  });
}

Fila prioritária

class PriorityQueue {
  #items = [];

  enqueue(item, priority) {
    this.#items.push({ item, priority });
    this.#items.sort((a, b) => a.priority - b.priority);
  }

  dequeue() {
    return this.#items.shift()?.item;
  }

  get length() {
    return this.#items.length;
  }
}

class PriorityCaptchaQueue {
  #apiKey;
  #maxConcurrent;
  #queue;
  #active;
  #results;

  constructor(apiKey, maxConcurrent = 5) {
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#queue = new PriorityQueue();
    this.#active = 0;
    this.#results = new Map();
  }

  submit(id, method, params, priority = 5) {
    return new Promise((resolve, reject) => {
      this.#queue.enqueue({ id, method, params, resolve, reject }, priority);
      this.#drain();
    });
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#queue.length > 0) {
      const task = this.#queue.dequeue();
      this.#active++;

      solveSingle(task.method, task.params)
        .then((token) => {
          this.#results.set(task.id, { status: "solved", token });
          task.resolve(token);
        })
        .catch((err) => {
          this.#results.set(task.id, { status: "error", error: err.message });
          task.reject(err);
        })
        .finally(() => {
          this.#active--;
          this.#drain();
        });
    }
  }
}

// Usage: high-priority checkout, low-priority scraping
const pq = new PriorityCaptchaQueue("YOUR_API_KEY", 3);

// Priority 1 (highest) — checkout
const checkoutToken = pq.submit(
  "checkout_1",
  "turnstile",
  { sitekey: "KEY", pageurl: "https://shop.com/checkout" },
  1
);

// Priority 5 (normal) — product scraping
for (let i = 0; i < 5; i++) {
  pq.submit(
    `product_${i}`,
    "userrecaptcha",
    { googlekey: "KEY", pageurl: `https://shop.com/p/${i}` },
    5
  );
}

Tentar novamente a fila com tratamento de mensagens mortas

class RetryQueue {
  #apiKey;
  #maxRetries;
  #results;
  #deadLetter;

  constructor(apiKey, maxRetries = 3) {
    this.#apiKey = apiKey;
    this.#maxRetries = maxRetries;
    this.#results = [];
    this.#deadLetter = [];
  }

  async processBatch(tasks, maxConcurrent = 5) {
    const queue = tasks.map((t) => ({ ...t, attempts: 0 }));

    while (queue.length > 0) {
      const batch = queue.splice(0, maxConcurrent);
      const results = await Promise.allSettled(
        batch.map((task) => this.#solveWithRetry(task))
      );

      for (let i = 0; i < results.length; i++) {
        const result = results[i];
        const task = batch[i];

        if (result.status === "fulfilled") {
          this.#results.push({ id: task.id, token: result.value });
        } else {
          task.attempts++;
          if (task.attempts < this.#maxRetries) {
            queue.push(task); // Retry
            console.log(`Retry ${task.attempts}/${this.#maxRetries}: ${task.id}`);
          } else {
            this.#deadLetter.push({
              id: task.id,
              error: result.reason.message,
              attempts: task.attempts,
            });
          }
        }
      }
    }

    return {
      solved: this.#results,
      failed: this.#deadLetter,
    };
  }

  async #solveWithRetry(task) {
    return solveSingle(task.method, task.params);
  }
}

Painel de monitoramento

class QueueMonitor {
  #startTime;
  #solveTimes;

  constructor() {
    this.#startTime = Date.now();
    this.#solveTimes = [];
    this.counts = { submitted: 0, solving: 0, solved: 0, failed: 0 };
  }

  recordSubmit() {
    this.counts.submitted++;
    this.counts.solving++;
  }

  recordSolved(solveTime) {
    this.counts.solving--;
    this.counts.solved++;
    this.#solveTimes.push(solveTime);
  }

  recordFailed() {
    this.counts.solving--;
    this.counts.failed++;
  }

  report() {
    const elapsed = (Date.now() - this.#startTime) / 1000;
    const avgTime =
      this.#solveTimes.length > 0
        ? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length
        : 0;
    const throughput = this.counts.solved / (elapsed / 60);
    const successRate =
      this.counts.solved + this.counts.failed > 0
        ? (this.counts.solved / (this.counts.solved + this.counts.failed)) * 100
        : 0;

    return {
      elapsed: `${elapsed.toFixed(0)}s`,
      submitted: this.counts.submitted,
      solving: this.counts.solving,
      solved: this.counts.solved,
      failed: this.counts.failed,
      avgSolveTime: `${(avgTime / 1000).toFixed(1)}s`,
      throughput: `${throughput.toFixed(1)}/min`,
      successRate: `${successRate.toFixed(1)}%`,
    };
  }
}

Solução de problemas

Sintoma Causa Correção
Todas as promessas rejeitadas simultaneamente Limite de taxa de API atingido Inferior maxConcurrent
A memória cresce com o tempo Resultados acumulando Processe e limpe os resultados periodicamente
A fila diminui, mas as tarefas permanecem Chamada drain() ausente após a conclusão Verifique o gatilho de drenagem no bloco final
ERROR_NO_SLOT_AVAILABLE Muitas chamadas de API simultâneas Adicionar atraso entre envios
A fila de mensagens mortas é preenchida Erros persistentes Verifique os tipos de erros – pode precisar de correção de parâmetros

Perguntas frequentes

Quantas soluções simultâneas devo executar?

Comece com 5 a 10 e aumente com base no seu plano CaptchaAI. Observe ERROR_NO_SLOT_AVAILABLE como o sinal do acelerador.

Devo usar uma biblioteca como p-queue ou bull?

Para casos de uso simples, os padrões integrados acima são suficientes. Use bull ou bullmq para filas persistentes apoiadas por Redis em configurações de produção de vários servidores.

Como lidar com a contrapressão da fila?

Limite o tamanho da fila e rejeite ou adie novos envios quando estiverem cheios. O padrão ConcurrencyQueue lida com isso naturalmente.


Resumo

Node.js é excelente em I/O simultâneo - perfeito para sistemas de fila CAPTCHA comCaptchaAI. Use Promise.allSettled para lotes simples, EventEmitter para rastreamento de progresso, filas de prioridade para fluxos críticos para os negócios e filas de repetição para confiabilidade.

Artigos relacionados

  • Construindo fila de resolução de Captcha Python

Próximos passos

  • Início rápido do CaptchaAI: sua primeira resolução de CAPTCHA em 5 minutos
  • Como resolver reCAPTCHA v2 com a API: guia passo a passo
  • Como resolver Cloudflare Turnstile pela API
  • Como resolver GeeTest v3 usando API
Os comentários estão desativados para este artigo.