Bu hafta başarı oranınız kaç? Hangi CAPTCHA türü en yavaş çözülüyor ve maliyetiniz neden dün iki katına çıktı? Günde binlerce CAPTCHA çözen bir iş akışında bu soruları tahminle değil veriyle yanıtlamak istiyorsanız, her çözüm denemesini MongoDB'ye kaydedin — sonrasında bu sorular tek satırlık aggregation sorgularına dönüşür.
Bu rehber, çözüm kayıtları için bir belge şeması tasarlamayı, doğru dizinleri kurmayı, çözüm ve saklama akışını yazmayı ve başarı oranı, çözüm süresi ile hata dağılımını çıkaran analitik sorguları adım adım gösteriyor. Örnekler Python ve Node.js için hazır.
CAPTCHA çözüm loglarını neden MongoDB'de tutmalısınız?
CAPTCHA çözüm kayıtları türe göre farklı alanlar taşır: reCAPTCHA googlekey ister, Turnstile sitekey ister, görüntü CAPTCHA'ları ham body ister. İlişkisel bir tabloda bu değişkenlik ya onlarca boş sütuna ya da sürekli şema geçişlerine yol açar. MongoDB'nin şemasız belgeleri bu alanları doğal biçimde, geçiş yapmadan işler.
Aynı esneklik analitik tarafında da işe yarar. Zaman serisi, gruplama ve koşullu sayım gereken sorgular MongoDB'nin aggregation çerçevesinde birkaç aşamayla ifade edilebilir; başarı oranını, türe göre ortalama çözüm süresini veya saatlik hacmi ayrı bir analitik altyapısı kurmadan aynı koleksiyondan çekersiniz.
Ne saklamalısınız: belge şeması
Her çözüm denemesini tek bir belge olarak saklayın. Kimlik, tür, durum, zaman damgaları, geçen süre ve serbest biçimli bir metadata alanı — proje, worker ve hedef alan adı gibi bağlamı bu alana koyarsınız. Bu yapı hem tekil kayıt sorgularını hem de gruplu analitiği besler:
{
"_id": "ObjectId",
"captcha_id": "12345678",
"type": "recaptcha_v2",
"method": "userrecaptcha",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"pageurl": "https://example.com/form",
"status": "solved",
"solution": "03AGdBq26...",
"error": null,
"submitted_at": "2026-04-04T10:15:30.000Z",
"solved_at": "2026-04-04T10:15:45.000Z",
"elapsed_ms": 15000,
"polls": 3,
"proxy_used": true,
"cost": 0.00299,
"metadata": {
"project": "price-monitor",
"worker_id": "worker-3",
"target_domain": "example.com"
}
}
status alanı akışın kalbidir: submitted → polling → solved/error/timeout. Analitik sorgularınızın çoğu bu alan üzerinde gruplama yapar, bu yüzden değerlerini baştan tutarlı tutun.
Python ile çözüm kaydı ve loglama
MongoDB bağlantısını kurun
Önce istemciyi, veritabanını ve solves koleksiyonunu tanımlayın. API anahtarınızı ve bağlantı dizesini koda gömmeyin — ortam değişkenlerinden okuyun:
import os
import time
from datetime import datetime, timezone
from pymongo import MongoClient, ASCENDING, DESCENDING
import requests
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017")
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
client = MongoClient(MONGO_URI)
db = client["captcha_tracking"]
solves = db["solves"]
Sorguları hızlandıran dizinleri oluşturun
Koleksiyon büyüdükçe dizinsiz aggregation sorguları tüm belgeleri taramak zorunda kalır. En sık sorgulanan alanlara dizin koyun ve eski kayıtları otomatik silmek için submitted_at üzerinde bir TTL dizini tanımlayın:
def setup_indexes():
solves.create_index([("submitted_at", DESCENDING)])
solves.create_index([("type", ASCENDING), ("status", ASCENDING)])
solves.create_index([("metadata.project", ASCENDING)])
solves.create_index([("metadata.target_domain", ASCENDING)])
solves.create_index(
[("submitted_at", ASCENDING)],
expireAfterSeconds=90 * 24 * 3600, # Auto-delete after 90 days
name="ttl_cleanup"
)
setup_indexes()
Çözün, saklayın ve durumu güncelleyin
Aşağıdaki fonksiyon önce belgeyi submitted durumuyla ekler, görevi CaptchaAI in.php uç noktasına gönderir, ardından res.php üzerinden sonucu periyodik olarak sorgular. Her adımda aynı belgeyi güncelleyerek geçen süreyi ve sorgulama sayısını kaydeder — yani her çözümün tam yaşam döngüsü tek bir kayıtta kalır:
def solve_and_store(sitekey, pageurl, captcha_type="recaptcha_v2", metadata=None):
record = {
"type": captcha_type,
"method": "userrecaptcha",
"sitekey": sitekey,
"pageurl": pageurl,
"status": "submitted",
"submitted_at": datetime.now(timezone.utc),
"metadata": metadata or {}
}
result = solves.insert_one(record)
doc_id = result.inserted_id
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
solves.update_one(
{"_id": doc_id},
{"$set": {"status": "error", "error": data.get("request")}}
)
return None
captcha_id = data["request"]
solves.update_one(
{"_id": doc_id},
{"$set": {"captcha_id": captcha_id, "status": "polling"}}
)
# Poll for result
polls = 0
for _ in range(60):
time.sleep(5)
polls += 1
poll_resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if poll_resp.get("status") == 1:
solved_at = datetime.now(timezone.utc)
elapsed_ms = int(
(solved_at - record["submitted_at"]).total_seconds() * 1000
)
solves.update_one({"_id": doc_id}, {"$set": {
"status": "solved",
"solution": poll_resp["request"],
"solved_at": solved_at,
"elapsed_ms": elapsed_ms,
"polls": polls
}})
return poll_resp["request"]
if poll_resp.get("request") != "CAPCHA_NOT_READY":
solves.update_one({"_id": doc_id}, {"$set": {
"status": "error",
"error": poll_resp.get("request"),
"polls": polls
}})
return None
solves.update_one({"_id": doc_id}, {"$set": {
"status": "timeout", "polls": polls
}})
return None
Analitik sorguları: başarı oranı ve çözüm süresi
Kayıtlar biriktikçe asıl değer aggregation sorgularında ortaya çıkar. Aşağıdaki dört fonksiyon sırasıyla son N saatteki başarı oranını, türe göre ortalama/en düşük/en yüksek çözüm süresini, çizim için saatlik hacmi ve hata kodlarının dağılımını verir:
def get_success_rate(hours=24):
"""Success rate for the last N hours."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}}},
{"$group": {
"_id": "$status",
"count": {"$sum": 1}
}}
]
results = {r["_id"]: r["count"] for r in solves.aggregate(pipeline)}
total = sum(results.values())
solved = results.get("solved", 0)
return (solved / total * 100) if total else 0
def get_avg_solve_time_by_type():
"""Average solve time grouped by CAPTCHA type."""
pipeline = [
{"$match": {"status": "solved"}},
{"$group": {
"_id": "$type",
"avg_time_ms": {"$avg": "$elapsed_ms"},
"min_time_ms": {"$min": "$elapsed_ms"},
"max_time_ms": {"$max": "$elapsed_ms"},
"count": {"$sum": 1}
}},
{"$sort": {"count": -1}}
]
return list(solves.aggregate(pipeline))
def get_hourly_solve_volume(days=7):
"""Hourly solve volume for charting."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}}},
{"$group": {
"_id": {
"date": {"$dateToString": {"format": "%Y-%m-%d", "date": "$submitted_at"}},
"hour": {"$hour": "$submitted_at"}
},
"total": {"$sum": 1},
"solved": {"$sum": {"$cond": [{"$eq": ["$status", "solved"]}, 1, 0]}}
}},
{"$sort": {"_id.date": 1, "_id.hour": 1}}
]
return list(solves.aggregate(pipeline))
def get_error_breakdown(hours=24):
"""Error frequency by error code."""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
pipeline = [
{"$match": {"submitted_at": {"$gte": cutoff}, "status": "error"}},
{"$group": {"_id": "$error", "count": {"$sum": 1}}},
{"$sort": {"count": -1}}
]
return list(solves.aggregate(pipeline))
get_avg_solve_time_by_type ile thread doluluğunuzu birlikte okumak pratik bir karar aracıdır. Örneğin İstanbul'daki bir e-ticaret ekibi ödeme akışı QA testlerinde günde birkaç bin reCAPTCHA çözüyorsa, türe göre çözüm süresi ve eşzamanlı iş yükü verisi BASIC ($15/ay, 5 thread) planının mı yoksa ADVANCE ($90/ay, 50 thread) planının mı gerektiğini net biçimde gösterir. Fiyatlar USD üzerinden ve thread bazlı olduğu için — kur dalgalanmasından bağımsız — aylık maliyeti önceden görürsünüz.
Node.js ile aynı akış
Node.js tarafında da mantık aynıdır: bağlan, dizinleri kur, çöz ve sakla, sonra sorgula. Sürücü olarak resmî mongodb paketini ve HTTP için axios kullanın:
const { MongoClient } = require("mongodb");
const axios = require("axios");
const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017";
const API_KEY = process.env.CAPTCHAAI_API_KEY;
let db, solves;
async function connect() {
const client = await MongoClient.connect(MONGO_URI);
db = client.db("captcha_tracking");
solves = db.collection("solves");
await solves.createIndex({ submitted_at: -1 });
await solves.createIndex({ type: 1, status: 1 });
await solves.createIndex({ "metadata.project": 1 });
await solves.createIndex(
{ submitted_at: 1 },
{ expireAfterSeconds: 90 * 24 * 3600 }
);
}
async function solveAndStore(sitekey, pageurl, type = "recaptcha_v2", metadata = {}) {
const submittedAt = new Date();
const { insertedId } = await solves.insertOne({
type, method: "userrecaptcha", sitekey, pageurl,
status: "submitted", submitted_at: submittedAt, metadata,
});
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method: "userrecaptcha", googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: submit.data.request } });
return null;
}
const captchaId = submit.data.request;
await solves.updateOne({ _id: insertedId }, { $set: { captcha_id: captchaId, status: "polling" } });
let polls = 0;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
polls++;
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
const solvedAt = new Date();
await solves.updateOne({ _id: insertedId }, { $set: {
status: "solved", solution: poll.data.request,
solved_at: solvedAt, elapsed_ms: solvedAt - submittedAt, polls,
}});
return poll.data.request;
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
await solves.updateOne({ _id: insertedId }, { $set: { status: "error", error: poll.data.request, polls } });
return null;
}
}
await solves.updateOne({ _id: insertedId }, { $set: { status: "timeout", polls } });
return null;
}
async function getSuccessRate(hours = 24) {
const cutoff = new Date(Date.now() - hours * 3600 * 1000);
const pipeline = [
{ $match: { submitted_at: { $gte: cutoff } } },
{ $group: { _id: "$status", count: { $sum: 1 } } },
];
const results = await solves.aggregate(pipeline).toArray();
const total = results.reduce((s, r) => s + r.count, 0);
const solved = results.find((r) => r._id === "solved")?.count || 0;
return total ? ((solved / total) * 100).toFixed(1) : 0;
}
Veri saklama ve TTL stratejisi
Ne kadar veri sakladığınız hem depolama maliyetinizi hem de uyumluluk yükünüzü belirler. Ortamınıza göre bir TTL süresi seçin:
| Strateji | TTL Endeksi | Kullanım Örneği |
|---|---|---|
| 30 günlük saklama | expireAfterSeconds: 2592000 |
Geliştirme/testing |
| 90 günlük saklama | expireAfterSeconds: 7776000 |
Üretim analitiği |
| Kalıcı (arşivli) | TTL yok; kapaklı toplama veya soğuk hava deposu kullanın | Uyumluluk/audit |
Bir uyarı: kayıtlarınız hedef alan adları veya kişisel veri içeriyorsa bu veriler KVKK kapsamına girebilir. Çözüm token'ları hedef sitede kısa sürede geçersiz olduğu için uzun vadeli analizde işe yaramaz — yalnızca meta veriyi (tür, süre, durum, hata) tutun, ham token'ları ve gereksiz kişisel veriyi TTL ile temizletin.
Sorun giderme
| Sorun | Sebep | Düzeltme |
|---|---|---|
| Yavaş toplama sorguları | submitted_at ve type'de eksik dizinler |
setup_indexes()'yi çalıştırın - yukarıdaki dizin bölümüne bakın |
| Belgeler büyüyor | Her kayıtta tam çözümlerin saklanması | Çözüm karmalarını saklayın veya kullanımdan sonra kesin |
| TTL eski kayıtları silmiyor | TTL monitör her 60 saniyede bir çalışır; büyük birikmiş işler zaman alır | Arka plan temizliğini bekleyin; db.solves.getIndexes() ile endeksi kontrol edin |
| Bağlantı havuzunun tükenmesi | Çok fazla eşzamanlı çözümleme işlemi | Bağlantı dizesinde maxPoolSize'yi ayarlayın |
Sık sorulan sorular
Çözüm token'larını ne kadar süre saklamalıyım?
Hata ayıklama için token'ları 24–48 saat tutmak yeterlidir; sonrasında TTL dizininin temizlemesine izin verin. Uzun vadeli analizde token değil, yalnızca meta veri saklayın — token hedef sitede zaten kısa sürede geçersiz olur.
Sakladığım CAPTCHA verileri KVKK kapsamına girer mi?
Kayıtlarınız kişisel veri ya da kişiyi işaret eden hedef URL'ler içeriyorsa girebilir. En güvenli yaklaşım, kayıtları teknik meta veriyle sınırlamak ve saklamayı yetkili QA/veri toplama iş akışlarınızla sınırlı tutmaktır.
Analitik sorgularım neden yavaş çalışıyor?
Neredeyse her zaman eksik dizin yüzündendir. submitted_at, type ve status alanlarına dizin koyduğunuzdan emin olun; aggregation'ın ilk $match aşaması dizini kullanabildiğinde sorgu tüm koleksiyonu taramaz.
Zaman damgalarını Europe/Istanbul saatine göre nasıl raporlarım?
Kayıtları her zaman UTC olarak saklayın (kod bunu yapıyor) ve yalnızca raporlama katmanında Europe/Istanbul saat dilimine çevirin. Böylece aynı veriyi farklı ekipler kendi saat dilimlerinde tutarlı biçimde okur.
MongoDB Atlas (bulut) ile çalışır mı?
Evet. Atlas hem TTL dizinlerini hem de aggregation işlem hatlarını destekler. Atlas kontrol panelinizdeki bağlantı dizesini MONGO_URI değişkenine koymanız yeterli.
Sonraki adımlar
Her CAPTCHA çözümünü kaydettiğinizde başarı oranındaki düşüşü, yavaşlayan bir türü veya artan maliyeti işlem hattınızı etkilemeden önce fark edersiniz. Başlamak için CaptchaAI API anahtarınızı alın ve ilk çözümünüzü koleksiyona yazın.
İlgili kılavuzlar:
- SQLite ile yerel CAPTCHA önbelleği
- Redis ile token TTL yönetimi
- Zaman serisiyle çözüm performansı trendleri