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; olaysolutionvedurationalanlarını taşır.failed— gönderim ya da çözüm hata verdi.timeout—maxWaitsü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
- istemci CAPTCHA işlem hatları oluşturma rehberi
- CAPTCHA çözüm sürelerini karşılaştırma
- sorumlu otomasyon oluşturma
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: