API Tutorials

Node.js Promise.allSettled ile toplu CAPTCHA çözme

Node.js'te onlarca, hatta yüzlerce CAPTCHA'yı tek seferde çözmeniz gerektiğinde doğru araç bellidir: Promise.allSettled. Nedeni basit — Promise.all görevlerden yalnızca biri başarısız olduğunda tüm partiyi reddeder ve o ana kadar çözülmüş sonuçları bile elinizden alır. Promise.allSettled ise her sözün tamamlanmasını bekler; hangi görevin çözüldüğünü, hangisinin neden başarısız olduğunu size tek tek bildirir.

Bunu somutlaştırmak için tipik bir senaryo düşünün: bir e-ticaret QA akışında 50 farklı ürün sayfasındaki reCAPTCHA doğrulamalarını paralel işliyorsunuz ve teslim tarihi yaklaşıyor. Beşinci istekte geçici bir zaman aşımı, Promise.all ile tüm işi çöpe atar. Promise.allSettled ile 49 başarılı token elinizde kalır, yalnızca tek görevi yeniden denersiniz. Freelance otomasyon işini deadline'a yetiştiren bir geliştirici için bu fark, saatler kazandırır.

Aşağıda temel toplu çözüm fonksiyonundan eşzamanlılık kontrolüne, yeniden denemeden ilerleme takibine kadar üretime hazır bir Node.js akışını adım adım kuruyoruz. Örnekler axios ve CaptchaAI API'si üzerine kuruludur.

Promise.all mı, yoksa Promise.allSettled mı?

İki yapının davranışı arasındaki fark, toplu işlerde her şeyi belirler. Promise.all bir "ya hep ya hiç" sözleşmesidir; Promise.allSettled ise kısmi başarıyı normal kabul eder.

// Promise.all — REJECTS if ANY task fails
const results = await Promise.all(tasks.map(solve)); // Throws on first error

// Promise.allSettled — RESOLVES always, with status for each
const results = await Promise.allSettled(tasks.map(solve));
// [{status: "fulfilled", value: "..."}, {status: "rejected", reason: Error}]

CAPTCHA çözümü doğası gereği ağ üzerinden yapılan, ara sıra zaman aşımına uğrayan bir işlemdir — yani kısmi başarısızlık istisna değil, beklenen durumdur. Bu yüzden toplu senaryoda tercih net:

Yöntem İlk hatada Döndürdüğü değer En uygun senaryo
Promise.all Anında reddeder Hiçbir şey (hata fırlatır) Ya hep ya hiç işlemleri
Promise.allSettled Devam eder Her görevin sonucu Toplu CAPTCHA çözme

Temel toplu çözüm fonksiyonunu kurun

İlk adım, tek bir CAPTCHA'yı gönderip sonucu sorgulayan bir fonksiyon ve bunu tüm partiye uygulayan bir sarmalayıcıdır. solveCaptcha, görevi in.php uç noktasına gönderir, ardından res.php'yi çözüm hazır olana kadar periyodik olarak sorgular. batchSolve ise bu çağrıları Promise.allSettled altında toplar ve sonucu çözülenler ile başarısızlar olarak ikiye ayırır.

const axios = require("axios");

const API_KEY = process.env.CAPTCHAAI_API_KEY;

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

async function solveCaptcha(sitekey, pageurl) {
  // Submit
  const submitResp = await axios.post(
    "https://ocr.captchaai.com/in.php",
    null,
    {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: pageurl,
        json: 1,
      },
    }
  );

  if (submitResp.data.status !== 1) {
    throw new Error(submitResp.data.request);
  }

  const captchaId = submitResp.data.request;

  // Poll
  for (let i = 0; i < 60; i++) {
    await sleep(5000);
    const result = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
    });

    if (result.data.status === 1) return result.data.request;
    if (result.data.request !== "CAPCHA_NOT_READY") {
      throw new Error(result.data.request);
    }
  }

  throw new Error("TIMEOUT");
}

async function batchSolve(tasks) {
  const promises = tasks.map((task) =>
    solveCaptcha(task.sitekey, task.pageurl).then((solution) => ({
      ...task,
      solution,
    }))
  );

  const results = await Promise.allSettled(promises);

  const solved = [];
  const failed = [];

  for (let i = 0; i < results.length; i++) {
    if (results[i].status === "fulfilled") {
      solved.push(results[i].value);
    } else {
      failed.push({
        task: tasks[i],
        error: results[i].reason.message,
      });
    }
  }

  return { solved, failed };
}

// Usage
(async () => {
  const tasks = [
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/1",
    },
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/2",
    },
    {
      sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageurl: "https://example.com/page/3",
    },
  ];

  const { solved, failed } = await batchSolve(tasks);
  console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);

  for (const s of solved) {
    console.log(`  ✓ ${s.pageurl}: ${s.solution.substring(0, 30)}...`);
  }
  for (const f of failed) {
    console.log(`  ✗ ${f.task.pageurl}: ${f.error}`);
  }
})();

Sonuçların gönderim sırasıyla eşleşmesi için dizin (i) üzerinden çalıştığımıza dikkat edin — Promise.allSettled sonuç dizisini gönderim sırasında döndürür, ama görevlerin tamamlanma sırası farklı olabilir.

Eşzamanlılığı thread sayınıza göre sınırlayın

1.000 CAPTCHA'yı aynı anda göndermek hem yerel bağlantı havuzunuzu hem de sunucu tarafını gereksiz yere zorlar. Çözüm bir eşzamanlılık sınırlayıcıdır: aynı anda yalnızca belirli sayıda görevin uçuşta olmasına izin verirsiniz.

Bu sınırı seçerken CaptchaAI planınızın thread sayısını ölçü alın. CaptchaAI thread bazlı faturalandırır: bir thread, o anda çözülmekte olan tek bir CAPTCHA demektir ve plan başına thread sayısı bellidir — örneğin ADVANCE ($90/ay, 50 thread) planında 50 thread'iniz vardır. Eşzamanlılığı thread sayınızın üzerine çıkarmanın bir faydası yoktur; fazladan istekler yalnızca kuyrukta bekler. Pratik bir başlangıç noktası, eşzamanlılığı plan thread sayınıza yakın ama biraz altında tutmaktır.

async function batchSolveWithLimit(tasks, concurrency = 10) {
  const results = [];
  let index = 0;

  async function worker() {
    while (index < tasks.length) {
      const i = index++;
      const task = tasks[i];

      try {
        const solution = await solveCaptcha(task.sitekey, task.pageurl);
        results[i] = { status: "fulfilled", value: { ...task, solution } };
      } catch (err) {
        results[i] = { status: "rejected", reason: err };
      }
    }
  }

  // Launch concurrent workers
  const workers = Array.from({ length: concurrency }, () => worker());
  await Promise.allSettled(workers);

  const solved = results
    .filter((r) => r.status === "fulfilled")
    .map((r) => r.value);
  const failed = results
    .filter((r) => r.status === "rejected")
    .map((r, i) => ({ task: tasks[i], error: r.reason.message }));

  return { solved, failed };
}

// Solve 100 CAPTCHAs, 10 at a time
const { solved, failed } = await batchSolveWithLimit(tasks, 10);

Burada sabit sayıda worker başlatıp hepsinin aynı görev kuyruğundan beslenmesini sağlıyoruz; böylece uçuştaki istek sayısı hiçbir zaman concurrency değerini aşmaz.

Başarısız görevleri otomatik yeniden deneyin

Zaman aşımı veya "boş slot yok" gibi hatalar çoğunlukla geçicidir — aynı görevi birkaç saniye sonra yeniden gönderdiğinizde çözülür. Kalıcı hataları (örneğin geçersiz sitekey) yeniden denemenin ise anlamı yoktur. Aşağıdaki fonksiyon yalnızca geçici hataları filtreleyip sınırlı sayıda yeniden dener.

async function batchSolveWithRetry(tasks, maxRetries = 2, concurrency = 10) {
  let currentTasks = [...tasks];
  let allSolved = [];

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    if (currentTasks.length === 0) break;

    console.log(
      `Attempt ${attempt + 1}: solving ${currentTasks.length} tasks...`
    );

    const { solved, failed } = await batchSolveWithLimit(
      currentTasks,
      concurrency
    );

    allSolved = [...allSolved, ...solved];

    // Only retry transient errors
    const retryable = failed.filter(
      (f) =>
        f.error === "TIMEOUT" ||
        f.error === "ERROR_NO_SLOT_AVAILABLE" ||
        f.error === "ERROR_TOO_MUCH_REQUESTS"
    );

    currentTasks = retryable.map((f) => f.task);

    if (retryable.length > 0) {
      console.log(`  Retrying ${retryable.length} failed tasks...`);
    }
  }

  const finalFailed = currentTasks; // Anything left after all retries
  return { solved: allSolved, failed: finalFailed };
}

Yeniden deneme sayısını (maxRetries) ölçülü tutun: üstel geri çekilme (exponential backoff) mantığıyla birleştirmediğiniz sürece agresif tekrar, geçici bir yoğunluğu daha da kötüleştirebilir.

Gerçek zamanlı ilerleme takibi

Yüzlerce görevlik bir parti dakikalar sürebilir. İşlemin donduğunu değil ilerlediğini görmek için tamamlanan görev sayısını canlı raporlamak faydalıdır. Aşağıdaki sarmalayıcı her görev bittiğinde konsola tek satırlık bir ilerleme çıktısı yazar.

async function batchSolveWithProgress(tasks, concurrency = 10) {
  let completed = 0;
  let succeeded = 0;
  let failed = 0;

  const wrapped = tasks.map((task) =>
    solveCaptcha(task.sitekey, task.pageurl)
      .then((solution) => {
        succeeded++;
        completed++;
        process.stdout.write(
          `\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
        );
        return { ...task, solution };
      })
      .catch((err) => {
        failed++;
        completed++;
        process.stdout.write(
          `\rProgress: ${completed}/${tasks.length} (${succeeded} ok, ${failed} err)`
        );
        throw err;
      })
  );

  const results = await Promise.allSettled(wrapped);
  console.log("\nDone.");
  return results;
}

Toplu sonuçları kategorilere ayırın

Promise.allSettled size ham bir durum dizisi verir; bunu doğrudan iş mantığında kullanmak zordur. Sonuçları çözülenler, geçici hatalar ve kalıcı hatalar olarak üç kovaya ayırmak, sonraki adımı (yeniden dene / logla / atla) netleştirir.

function categorizeResults(settled, originalTasks) {
  const categories = {
    solved: [],
    transientErrors: [],
    permanentErrors: [],
  };

  const TRANSIENT = new Set([
    "TIMEOUT",
    "ERROR_NO_SLOT_AVAILABLE",
    "ERROR_TOO_MUCH_REQUESTS",
  ]);

  for (let i = 0; i < settled.length; i++) {
    const r = settled[i];
    if (r.status === "fulfilled") {
      categories.solved.push(r.value);
    } else {
      const error = r.reason.message;
      const entry = { task: originalTasks[i], error };

      if (TRANSIENT.has(error)) {
        categories.transientErrors.push(entry);
      } else {
        categories.permanentErrors.push(entry);
      }
    }
  }

  return categories;
}

Geçici hatalar yeniden deneme kuyruğuna, kalıcı hatalar ise inceleme için loglara gider — bu ayrım, gereksiz yeniden denemelerle thread'lerinizi boşa harcamanızı önler.

Sık karşılaşılan sorunlar ve çözümleri

Sorun Sebep Çözüm
Tüm görevler zaman aşımına uğradı Çok fazla eşzamanlı istek CaptchaAI'yi veya proxy'yi bunaltıyor Eşzamanlılığı 5–10 aralığına düşürün
ERR_SOCKET_EXHAUSTION Aynı anda açılan çok fazla HTTP bağlantısı http.AgentmaxSockets sınırıyla kullanın
Sonuç dizisi karışık geliyor Tamamlanma sırası gönderim sırasından farklı Dizin tabanlı sonuç saklamayı kullanın (yukarıda gösterildi)
Büyük partilerde bellek şişiyor Tüm sözlerin bellekte tutulması Görevleri 100–500'lük parçalara bölerek işleyin

Sık sorulan sorular

Eşzamanlılığı planımdaki thread sayısına göre mi ayarlamalıyım?

Evet, bu iyi bir başlangıç kuralıdır. CaptchaAI thread bazlı çalıştığı için eşzamanlılığı thread sayınızın üzerine çıkarmak ek hız getirmez; fazla istekler yalnızca kuyrukta bekler. Örneğin STANDARD ($30/ay, 15 thread) planında 10–15 eşzamanlılık makul bir tavandır.

Promise.allSettled bir görev hata verince neden çökmez?

Çünkü tasarımı gereği hiçbir zaman reddetmez; her görev için fulfilled ya da rejected durumunu içeren bir sonuç döndürür. Promise.all ilk hatada tüm zinciri fırlatırken, Promise.allSettled tüm görevler tamamlanana kadar bekler ve başarısızlıkları veri olarak size teslim eder.

Binlerce CAPTCHA'yı tek partide göndermek güvenli mi?

Tüm sözleri aynı anda bellekte tutmak büyük partilerde bellek tüketimini artırır. Onun yerine görevleri 100–500'lük parçalara bölün ve her parçayı eşzamanlılık sınırlayıcıyla işleyin; hem bellek sabit kalır hem de bağlantı havuzunuz rahat eder.

Hangi hataları yeniden denemeliyim?

Yalnızca geçici hataları: TIMEOUT, ERROR_NO_SLOT_AVAILABLE ve ERROR_TOO_MUCH_REQUESTS. Geçersiz sitekey gibi kalıcı hatalar her denemede aynı şekilde başarısız olur; bunları yeniden denemek yerine loglayıp elle inceleyin.

Sonraki adım

CAPTCHA'ları paralel çözün — CaptchaAI API anahtarınızı alın ve ilk toplu işinizi çalıştırın.

İlgili rehberler:

Bu makale için yorumlar devre dışı bırakılmıştır.