Tutorials

Node.js ve CaptchaAI ile CAPTCHA Çözme Olay Veriyolu Oluşturma

Bir CAPTCHA çözüm isteğinin hangi aşamada olduğunu koda serpiştirilmiş if bloklarıyla üç ayrı yerde takip etmek yerine, tek bir olay veri yolu (event bus) kurup uygulamanızın her parçasının duruma tepki vermesini sağlayabilirsiniz. Node.js'in yerleşik EventEmitter sınıfı bunun için ek bir kütüphane bile gerektirmez: gönderildi, beklemede, çözüldü, başarısız ve zaman aşımı olaylarını yayınlarsınız; loglama, metrik toplama ve yeniden deneme gibi dinleyiciler bu olaylara birbirinden bağımsız abone olur. Aşağıda, CaptchaAI API'siyle çalışan tam bir CaptchaBus sınıfını hem Node.js hem de Python için kuruyoruz.

Mimari: tek veri yolu, bağımsız dinleyiciler

Fikir basit: çözüm mantığı olayları yayınlar, geri kalan her şey bu olayları dinler. Çözüm kodu, kimin dinlediğini ya da kaç dinleyici olduğunu bilmez.

[CaptchaBus]
   ├── emit("submitted", { taskId, type, pageurl })
   ├── emit("pending", { taskId, elapsed })
   ├── emit("solved", { taskId, solution, duration })
   ├── emit("failed", { taskId, error, duration })
   └── emit("timeout", { taskId, elapsed })
        ↓          ↓           ↓
   [Logger]    [Metrics]   [Retry Handler]

Veri yolunun yaydığı beş olay şunlardır:

  • submitted — görev CaptchaAI'ye iletildi, sorgulama başladı.
  • pending — sonuç henüz hazır değil; her sorgulama turunda tetiklenir.
  • solved — token hazır; olay solution ve duration alanlarını taşır.
  • failed — gönderim ya da çözüm hata verdi.
  • timeoutmaxWait süresi aşıldı, çözüm gelmedi.

Dinleyiciler birbirinden bağımsız kaydolur. Yeni bir yetenek eklemek — örneğin metrik toplama — çözüm kodunda tek satır değişiklik gerektirmez; yalnızca yeni bir dinleyici bağlarsınız.

CaptchaBus sınıfı (Node.js)

const EventEmitter = require("events");
const axios = require("axios");

class CaptchaBus extends EventEmitter {
  constructor(apiKey, options = {}) {
    super();
    this.apiKey = apiKey;
    this.pollInterval = options.pollInterval || 5000;
    this.maxWait = options.maxWait || 300000; // 5 minutes
    this.pending = new Map();
  }

  async submit(params) {
    const { method, sitekey, pageurl, ...extra } = params;
    const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

    const submitParams = {
      key: this.apiKey,
      method: method || "userrecaptcha",
      googlekey: sitekey,
      pageurl: pageurl,
      json: 1,
      ...extra,
    };

    try {
      const resp = await axios.post(
        "https://ocr.captchaai.com/in.php",
        null,
        { params: submitParams }
      );

      if (resp.data.status !== 1) {
        this.emit("failed", {
          taskId,
          error: resp.data.request,
          duration: 0,
        });
        return null;
      }

      const captchaId = resp.data.request;
      const startTime = Date.now();

      this.emit("submitted", {
        taskId,
        captchaId,
        method: method || "userrecaptcha",
        pageurl,
      });

      // Start polling
      this._poll(taskId, captchaId, startTime);
      return taskId;
    } catch (err) {
      this.emit("failed", { taskId, error: err.message, duration: 0 });
      return null;
    }
  }

  async _poll(taskId, captchaId, startTime) {
    const check = async () => {
      const elapsed = Date.now() - startTime;

      if (elapsed > this.maxWait) {
        this.emit("timeout", { taskId, elapsed });
        return;
      }

      this.emit("pending", { taskId, elapsed });

      try {
        const resp = await axios.get("https://ocr.captchaai.com/res.php", {
          params: {
            key: this.apiKey,
            action: "get",
            id: captchaId,
            json: 1,
          },
        });

        if (resp.data.status === 1) {
          this.emit("solved", {
            taskId,
            captchaId,
            solution: resp.data.request,
            duration: Date.now() - startTime,
          });
        } else if (resp.data.request === "CAPCHA_NOT_READY") {
          setTimeout(check, this.pollInterval);
        } else {
          this.emit("failed", {
            taskId,
            error: resp.data.request,
            duration: Date.now() - startTime,
          });
        }
      } catch (err) {
        this.emit("failed", {
          taskId,
          error: err.message,
          duration: Date.now() - startTime,
        });
      }
    };

    setTimeout(check, this.pollInterval);
  }
}

module.exports = CaptchaBus;

submit görevi in.php uç noktasına gönderir, bir taskId üretir ve res.php üzerinden pollInterval aralıklarıyla sonucu sorgulamaya başlar. Her durum geçişi bir olayla dışarı yayılır; sınıfın kendisi bu olaylarla ne yapılacağını bilmez.

Olay dinleyicilerini kaydetme

Aynı olaya birden fazla dinleyici bağlayabilirsiniz. Aşağıda biri loglama, diğeri metrik toplama için iki bağımsız dinleyici seti var — ikisi de aynı solved olayını dinliyor ama birbirinden habersiz.

const CaptchaBus = require("./captcha-bus");

const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
  pollInterval: 5000,
  maxWait: 120000,
});

// Logging listener
bus.on("submitted", (e) => {
  console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});

bus.on("pending", (e) => {
  console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});

bus.on("solved", (e) => {
  console.log(
    `[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
  );
});

bus.on("failed", (e) => {
  console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});

bus.on("timeout", (e) => {
  console.error(
    `[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
  );
});

// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };

bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
  metrics.solved++;
  metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);

// Submit a CAPTCHA
bus.submit({
  sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  pageurl: "https://example.com",
});

Metrik nesnesini istediğiniz zaman bir panele ya da Europe/Istanbul saat diliminde damgalanmış bir JSONL loguna yazabilirsiniz; çözüm kodunu değiştirmenize gerek yoktur.

Python karşılığı: thread tabanlı sorgulama

Aynı deseni Python'da da kurabilirsiniz. Node.js EventEmitter'ı yerleşik olduğundan, Python tarafında küçük bir on/emit uygulaması yazıp sorgulamayı arka planda bir thread'e alıyoruz.

import os
import time
import threading
from collections import defaultdict
import requests


class CaptchaBus:
    def __init__(self, api_key, poll_interval=5, max_wait=300):
        self.api_key = api_key
        self.poll_interval = poll_interval
        self.max_wait = max_wait
        self._listeners = defaultdict(list)

    def on(self, event, callback):
        """Register a listener for an event."""
        self._listeners[event].append(callback)
        return self

    def emit(self, event, data):
        """Emit an event to all registered listeners."""
        for callback in self._listeners.get(event, []):
            try:
                callback(data)
            except Exception as e:
                print(f"Listener error on {event}: {e}")

    def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
        """Submit a CAPTCHA and begin tracking."""
        task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": method,
            "googlekey": sitekey,
            "pageurl": pageurl,
            "json": 1,
            **extra
        })
        data = resp.json()

        if data.get("status") != 1:
            self.emit("failed", {
                "task_id": task_id,
                "error": data.get("request"),
                "duration": 0
            })
            return None

        captcha_id = data["request"]
        start_time = time.time()

        self.emit("submitted", {
            "task_id": task_id,
            "captcha_id": captcha_id,
            "method": method,
            "pageurl": pageurl
        })

        # Poll in a background thread
        thread = threading.Thread(
            target=self._poll,
            args=(task_id, captcha_id, start_time),
            daemon=True
        )
        thread.start()
        return task_id

    def _poll(self, task_id, captcha_id, start_time):
        while True:
            elapsed = time.time() - start_time

            if elapsed > self.max_wait:
                self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
                return

            time.sleep(self.poll_interval)
            self.emit("pending", {"task_id": task_id, "elapsed": elapsed})

            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1
            })
            data = resp.json()

            if data.get("status") == 1:
                self.emit("solved", {
                    "task_id": task_id,
                    "solution": data["request"],
                    "duration": time.time() - start_time
                })
                return
            elif data.get("request") != "CAPCHA_NOT_READY":
                self.emit("failed", {
                    "task_id": task_id,
                    "error": data.get("request"),
                    "duration": time.time() - start_time
                })
                return


# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])

bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))

bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")

İki uygulama da aynı olay adlarını (submitted, pending, solved, failed, timeout) kullanır; böylece dinleyici sözleşmeniz dilden bağımsız kalır.

Yeniden deneme mantığını dinleyici olarak ekleme

Yeniden deneme, olay veri yolunun asıl gücünü gösterdiği yerdir: failed olayına bir dinleyici bağlayıp görevi yeniden gönderirsiniz. Çözüm kodunun retry'dan haberi bile olmaz.

// Automatic retry on failure
bus.on("failed", async (e) => {
  if (e.retryCount >= 3) {
    console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
    return;
  }

  console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
  await bus.submit({
    ...e.originalParams,
    _retryCount: (e.retryCount || 0) + 1,
  });
});

Promise tabanlı sarmalayıcı

Olay tabanlı akış esnek olsa da, kimi zaman await edilebilir tek bir çağrı istersiniz. Veri yolunun üstüne ince bir Promise katmanı geçirerek her iki dünyayı birleştirebilirsiniz.

function solveCaptcha(bus, params) {
  return new Promise((resolve, reject) => {
    const taskId = bus.submit(params);

    function onSolved(e) {
      if (e.taskId === taskId) {
        cleanup();
        resolve(e.solution);
      }
    }

    function onFailed(e) {
      if (e.taskId === taskId) {
        cleanup();
        reject(new Error(e.error));
      }
    }

    function cleanup() {
      bus.removeListener("solved", onSolved);
      bus.removeListener("failed", onFailed);
      bus.removeListener("timeout", onFailed);
    }

    bus.on("solved", onSolved);
    bus.on("failed", onFailed);
    bus.on("timeout", onFailed);
  });
}

// Usage
const solution = await solveCaptcha(bus, {
  sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  pageurl: "https://example.com",
});

cleanup fonksiyonuna dikkat edin: her çözüm sonrası dinleyicileri kaldırmazsanız, uzun süre çalışan bir serviste dinleyiciler birikir ve Node.js size bellek sızıntısı uyarısı verir.

Eşzamanlılığı planınızın thread sayısıyla eşleştirin

Olay veri yolu, aynı anda kaç çözümü izleyebileceğinizi kod tarafında sınırlamaz; gerçek sınır CaptchaAI planınızın thread sayısıdır. Bir thread, işlemdeki tek bir CAPTCHA demektir; çözüm bitince o thread bir sonrakini alır. Eşzamanlı submit çağrılarınızı ve pollInterval değerinizi planınızın thread bütçesine göre ölçekleyin:

  • BASIC ($15/ay, 5 thread) — aynı anda 5 çözüm
  • ADVANCE ($90/ay, 50 thread) — aynı anda 50 çözüm
  • PREMIUM ($170/ay, 100 thread) — aynı anda 100 çözüm

Her plan thread başına sınırsız çözüm içerir; ücret çözüm başına değil, eşzamanlı thread başına alınır.

Somut bir örnek: Türkiye'deki bir e-ticaret ekibi, ödeme akışını staging.example.com/qa-login gibi bir QA ortamında test ederken onlarca senaryoyu paralel çalıştırır. Olay veri yolu her senaryonun solved ve failed sayısını tek yerde toplar; thread sayısı ise aynı anda kaç senaryonun ilerleyebileceğini belirler. Aylık sabit USD fiyatlandırma, kur dalgalanmasından bağımsız öngörülebilir bir maliyet planlaması sağladığı için bu tür ekipler thread bütçesini net biçimde hesaplayabilir.

Sorun giderme

Sorun Sebep Çözüm
Dinleyici tetiklenmiyor emit ve on'daki olay adları birbirini tutmuyor (ör. "solve" ve "solved") Kullanılan olay adlarını emit/on çağrılarında birebir karşılaştırın
Bellek sızıntısı uyarısı Tek bir olaya çok fazla dinleyici bağlı setMaxListeners() ayarlayın ya da kullanım sonrası dinleyicileri kaldırın
pending olayları konsolu dolduruyor Sorgulama aralığı çok kısa pollInterval değerini 5000 ms veya üzerine çıkarın
Yeniden denemede olaylar kayboluyor Retry sırasında yeni bir taskId üretiliyor Durumu yeniden bağlamak için orijinal parametreleri aktarın

Sık sorulan sorular

EventEmitter yerine ne zaman Kafka veya Redis kullanmalıyım?

Tek işlemli (single-process) uygulamalarda işlem içi olay veri yolu (EventEmitter) daha basit ve daha hızlıdır. CAPTCHA olaylarına birden fazla işlemin veya servisin tepki vermesi gerektiğinde Kafka, RabbitMQ veya Redis gibi harici bir mesaj kuyruğuna geçin.

Olay veri yolu ile callback/webhook arasındaki fark nedir?

Callback ve webhook, sonucu tek bir dış uç noktaya iletir; olay veri yolu ise aynı durum değişikliğini uygulamanızın içinde istediğiniz kadar bağımsız dinleyiciye dağıtır. İkisini birlikte de kullanabilirsiniz: webhook'u dış sistemlere, veri yolunu uygulama içi tepkilere bırakın.

Aynı anda kaç CAPTCHA çözümünü tek veri yoluyla yönetebilirim?

Kod tarafında bir sınır yoktur; pratik sınır planınızın thread sayısıdır. BASIC ($15/ay, 5 thread) aynı anda 5 çözüm demektir; daha yüksek eşzamanlılık için thread sayısı daha fazla olan bir plana geçin.

CaptchaAI'yi hiç çağırmadan olayları test edebilir miyim?

Evet. Veri yolu yalnızca bir EventEmitter olduğundan, HTTP çağrılarını taklit edip testlerinizde doğrudan bus.emit("solved", {...}) çağırabilir ve dinleyici davranışını doğrulayabilirsiniz.

İlgili makaleler

Sonraki adımlar

Olay odaklı CAPTCHA hatlarınızı kurun — CaptchaAI API anahtarınızı alın ve veri yolunuzun dinleyicilerini bağlayın.

İlgili kılavuzlar:

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