CaptchaAI planları çözüm başına değil thread başına faturalandırıldığı için bir otomasyon işinde gerçekten ölçmeniz gereken üç şey var: kaç çözüm gönderdiniz, ortalama çözüm süresi ne kadar ve hangi token'lar kullanılmadan süresi dolup gitti. Üçünün de yanıtı tek bir .db dosyasının içindedir; Python'un standart kütüphanesindeki sqlite3, iki tablo ve birkaç indeksle günde birkaç bin çözüm yapan bir iş yükünü rahatça taşır.
Aşağıda şema tasarımından temizlik rutinine kadar çalışan bir kurulum var: gönderim kaydı, TTL'li token önbelleği, analitik sorgular ve Node.js karşılığı. Redis veya PostgreSQL'e geçiş vaktinin geldiğini nereden anlayacağınızı da baştan söylüyoruz.
Tek dosyalık veritabanı hangi iş yükünde yeter?
SQLite'ın sınırı yazma hızından çok eşzamanlılıktır. Tek makinede çalışan bir worker için fazlasıyla iyidir; aynı tabloya farklı sunuculardan yazmaya başladığınız anda sınıra dayanırsınız.
| Kullanım senaryosu | SQLite | Daha uygun alternatif |
|---|---|---|
| Tek makinede geliştirme ve hata ayıklama | ✅ | — |
| Küçük ölçekli üretim (saatte < 1K çözüm) | ✅ | — |
| Test koşularının sonuç kaydı | ✅ | — |
| Çok sunuculu üretim | ❌ | PostgreSQL, MongoDB |
| Yüksek hacimli dağıtık iş yükü | ❌ | Redis, DynamoDB |
| Gerçek zamanlı analitik panosu | ❌ | TimescaleDB, InfluxDB |
Pratik eşik şu: iş yükünüz tek makineye sığıyorsa ve saatlik çözüm sayınız dört haneye çıkmıyorsa SQLite'ta kalın. Dosyayı yedeklemek cp kadar basit, taşınabilirliği de cabası.
Şema: iki tablo, dört indeks
Şemayı iki sorumluluğa ayırın. captcha_solves her gönderimin geçmişini tutar (tip, sitekey, durum, süre, sorgulama sayısı); token_cache yalnızca hâlâ geçerli olan token'ları saklar. İkisini tek tabloda birleştirmek, temizlik rutinini yazarken başınızı ağrıtır.
CREATE TABLE IF NOT EXISTS captcha_solves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
captcha_id TEXT,
type TEXT NOT NULL,
sitekey TEXT,
pageurl TEXT,
status TEXT NOT NULL DEFAULT 'submitted',
solution TEXT,
error TEXT,
submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
solved_at TEXT,
elapsed_ms INTEGER,
polls INTEGER DEFAULT 0,
project TEXT
);
CREATE INDEX IF NOT EXISTS idx_submitted_at ON captcha_solves(submitted_at);
CREATE INDEX IF NOT EXISTS idx_type_status ON captcha_solves(type, status);
CREATE INDEX IF NOT EXISTS idx_sitekey ON captcha_solves(sitekey);
-- Token cache for reuse within TTL
CREATE TABLE IF NOT EXISTS token_cache (
sitekey TEXT NOT NULL,
pageurl TEXT NOT NULL,
token TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
used INTEGER DEFAULT 0,
PRIMARY KEY (sitekey, pageurl, token)
);
CREATE INDEX IF NOT EXISTS idx_cache_lookup
ON token_cache(sitekey, pageurl, used, expires_at);
used sütunu kritik: bir token'ı iki kez göndermek hedef tarafta hata döndürür, bu yüzden önbellekten okunan kayıt aynı işlemde işaretlenir. expires_at alanı ise TTL kontrolünü sorgunun içine taşır — uygulama tarafında tarih karşılaştırması yapmanıza gerek kalmaz.
Python uygulaması
Adım 1: Bağlantıyı ve şemayı hazırlayın
WAL modu kapalıyken eşzamanlı okuma-yazma yaptığınızda database is locked hatası kaçınılmazdır; busy_timeout ise kısa kilitlenmelerde isteği hemen düşürmek yerine beklemesini sağlar. İkisini de bağlantı açılışında bir kez ayarlayın.
import os
import time
import sqlite3
from datetime import datetime, timedelta, timezone
import requests
DB_PATH = os.environ.get("CAPTCHA_DB", "captcha_solves.db")
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL") # Better concurrent read performance
conn.execute("PRAGMA busy_timeout=5000")
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS captcha_solves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
captcha_id TEXT,
type TEXT NOT NULL,
sitekey TEXT,
pageurl TEXT,
status TEXT NOT NULL DEFAULT 'submitted',
solution TEXT,
error TEXT,
submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
solved_at TEXT,
elapsed_ms INTEGER,
polls INTEGER DEFAULT 0,
project TEXT
);
CREATE INDEX IF NOT EXISTS idx_submitted_at ON captcha_solves(submitted_at);
CREATE INDEX IF NOT EXISTS idx_type_status ON captcha_solves(type, status);
CREATE TABLE IF NOT EXISTS token_cache (
sitekey TEXT NOT NULL,
pageurl TEXT NOT NULL,
token TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT NOT NULL,
used INTEGER DEFAULT 0,
PRIMARY KEY (sitekey, pageurl, token)
);
CREATE INDEX IF NOT EXISTS idx_cache_lookup
ON token_cache(sitekey, pageurl, used, expires_at);
""")
conn.close()
init_db()
Adım 2: Görevi gönderin, sonucu sorgulayın, kaydedin
Akış sabittir: önce önbelleğe bakın, kayıt yoksa görevi in.php uç noktasına gönderin, dönen captcha_id ile res.php üzerinden periyodik sorgulama yapın ve sonucu aynı satıra yazın. Takip kaydı, istek gönderilmeden önce açılır — böylece süreç ortasında çöken bir worker bile geride iz bırakır.
def solve_recaptcha(sitekey, pageurl, project=None):
conn = get_db()
# Check cache first
cached = get_cached_token(conn, sitekey, pageurl)
if cached:
conn.close()
return cached
# Insert tracking record
now = datetime.now(timezone.utc).isoformat()
cursor = conn.execute(
"INSERT INTO captcha_solves (type, sitekey, pageurl, submitted_at, project) "
"VALUES (?, ?, ?, ?, ?)",
("recaptcha_v2", sitekey, pageurl, now, project)
)
row_id = cursor.lastrowid
conn.commit()
# 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:
conn.execute(
"UPDATE captcha_solves SET status=?, error=? WHERE id=?",
("error", data.get("request"), row_id)
)
conn.commit()
conn.close()
return None
captcha_id = data["request"]
conn.execute(
"UPDATE captcha_solves SET captcha_id=?, status=? WHERE id=?",
(captcha_id, "polling", row_id)
)
conn.commit()
# Poll
polls = 0
for _ in range(60):
time.sleep(5)
polls += 1
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
solved_at = datetime.now(timezone.utc).isoformat()
submitted = datetime.fromisoformat(now)
elapsed = int((datetime.now(timezone.utc) - submitted).total_seconds() * 1000)
conn.execute(
"UPDATE captcha_solves SET status=?, solution=?, solved_at=?, "
"elapsed_ms=?, polls=? WHERE id=?",
("solved", result["request"], solved_at, elapsed, polls, row_id)
)
# Cache the token
cache_token(conn, sitekey, pageurl, result["request"])
conn.commit()
conn.close()
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
conn.execute(
"UPDATE captcha_solves SET status=?, error=?, polls=? WHERE id=?",
("error", result.get("request"), polls, row_id)
)
conn.commit()
conn.close()
return None
conn.execute(
"UPDATE captcha_solves SET status=?, polls=? WHERE id=?",
("timeout", polls, row_id)
)
conn.commit()
conn.close()
return None
CAPCHA_NOT_READY dışındaki her yanıt bir hata kodudur ve doğrudan error sütununa yazılır. Bu tek alan sayesinde "hangi sitekey sürekli hata veriyor" sorusuna ileride tek GROUP BY ile yanıt verirsiniz.
Adım 3: Token'ı TTL süresince önbellekte tutun
reCAPTCHA token'larının ömrü kısadır; bu yüzden varsayılan TTL 90 saniyedir. Aynı sitekey + pageurl çifti için art arda gelen isteklerde önbellek, gereksiz bir çözüm görevini ve dolayısıyla bir thread'in boşuna meşgul edilmesini engeller.
def cache_token(conn, sitekey, pageurl, token, ttl_seconds=90):
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat()
conn.execute(
"INSERT OR REPLACE INTO token_cache (sitekey, pageurl, token, expires_at) "
"VALUES (?, ?, ?, ?)",
(sitekey, pageurl, token, expires_at)
)
def get_cached_token(conn, sitekey, pageurl):
now = datetime.now(timezone.utc).isoformat()
row = conn.execute(
"SELECT token FROM token_cache "
"WHERE sitekey=? AND pageurl=? AND used=0 AND expires_at > ? "
"ORDER BY expires_at ASC LIMIT 1",
(sitekey, pageurl, now)
).fetchone()
if row:
conn.execute(
"UPDATE token_cache SET used=1 WHERE token=?",
(row["token"],)
)
conn.commit()
return row["token"]
return None
Adım 4: Analitik sorguları ve temizlik rutini
Aynı veriden hem operasyonel hem finansal görünüm çıkar: son 24 saatteki gönderim sayısı, başarı oranı ve ortalama çözüm süresi. cleanup_old_records() fonksiyonunu günlük bir cron işine bağlayın; VACUUM çağrısı silinen satırların yerini fiilen geri kazandırır.
def get_stats(hours=24):
conn = get_db()
cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
total = conn.execute(
"SELECT COUNT(*) FROM captcha_solves WHERE submitted_at >= ?", (cutoff,)
).fetchone()[0]
solved = conn.execute(
"SELECT COUNT(*) FROM captcha_solves WHERE submitted_at >= ? AND status='solved'",
(cutoff,)
).fetchone()[0]
avg_time = conn.execute(
"SELECT AVG(elapsed_ms) FROM captcha_solves "
"WHERE submitted_at >= ? AND status='solved'",
(cutoff,)
).fetchone()[0]
conn.close()
return {
"total": total,
"solved": solved,
"success_rate": (solved / total * 100) if total else 0,
"avg_time_ms": round(avg_time) if avg_time else 0
}
def cleanup_old_records(days=30):
conn = get_db()
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
conn.execute("DELETE FROM captcha_solves WHERE submitted_at < ?", (cutoff,))
conn.execute("DELETE FROM token_cache WHERE expires_at < ?",
(datetime.now(timezone.utc).isoformat(),))
conn.execute("VACUUM")
conn.commit()
conn.close()
Ortalama çözüm süresini haftalık izleyin. Süre yukarı doğru kayıyorsa bu çoğu zaman koddaki bir sorun değil, thread'lerinizin dolması demektir — yani plan seviyesinin konuşulacağı andır.
Node.js tarafında aynı akış
Python dışındaki servisler için better-sqlite3 senkron API'siyle aynı şemayı kullanır. Tablolar birebir aynı olduğu için iki dil aynı .db dosyasını paylaşabilir.
const Database = require("better-sqlite3");
const axios = require("axios");
const db = new Database(process.env.CAPTCHA_DB || "captcha_solves.db");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
db.pragma("journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS captcha_solves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
captcha_id TEXT, type TEXT NOT NULL, sitekey TEXT, pageurl TEXT,
status TEXT DEFAULT 'submitted', solution TEXT, error TEXT,
submitted_at TEXT DEFAULT (datetime('now')),
solved_at TEXT, elapsed_ms INTEGER, polls INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_submitted ON captcha_solves(submitted_at);
`);
async function solveAndStore(sitekey, pageurl) {
const submittedAt = new Date().toISOString();
const insert = db.prepare(
"INSERT INTO captcha_solves (type, sitekey, pageurl, submitted_at) VALUES (?, ?, ?, ?)"
);
const { lastInsertRowid } = insert.run("recaptcha_v2", sitekey, pageurl, submittedAt);
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) {
db.prepare("UPDATE captcha_solves SET status=?, error=? WHERE id=?")
.run("error", submit.data.request, lastInsertRowid);
return null;
}
const captchaId = submit.data.request;
db.prepare("UPDATE captcha_solves SET captcha_id=?, status=? WHERE id=?")
.run(captchaId, "polling", lastInsertRowid);
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 elapsed = Date.now() - new Date(submittedAt).getTime();
db.prepare(
"UPDATE captcha_solves SET status=?, solution=?, solved_at=?, elapsed_ms=?, polls=? WHERE id=?"
).run("solved", poll.data.request, new Date().toISOString(), elapsed, polls, lastInsertRowid);
return poll.data.request;
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
db.prepare("UPDATE captcha_solves SET status=?, error=?, polls=? WHERE id=?")
.run("error", poll.data.request, polls, lastInsertRowid);
return null;
}
}
db.prepare("UPDATE captcha_solves SET status=?, polls=? WHERE id=?")
.run("timeout", polls, lastInsertRowid);
return null;
}
Türkiye'den bir senaryo: e-ticaret QA ekibi
İstanbul'da çalışan üç kişilik bir QA ekibi, müşterilerinin ödeme adımını her gece regresyon testinden geçiriyor. Test formlarındaki reCAPTCHA v2 doğrulaması CaptchaAI ile çözülüyor, her koşunun sonucu ise bu SQLite dosyasına yazılıyor. Zaman damgalarını Europe/Istanbul saat dilimine çevirdiklerinde sabah raporunda "gece 03.00 koşusunda ortalama süre iki katına çıktı" gibi bulgular anında görünür hale geliyor.
İki pratik not. Birincisi: test verilerinde gerçek müşteri bilgisi kullanmayın — kayıtlara düşen her kişisel veri KVKK kapsamına girer, +90 555 000 0000 gibi sahte değerlerle çalışın. İkincisi maliyet tarafı: CaptchaAI planları çözüm başına değil thread başına faturalandırılır. BASIC ($15/ay, 5 thread) ile başlayıp panelde eşzamanlılık sıkıştığında ADVANCE ($90/ay, 50 thread) seviyesine çıkmak, kur dalgalanmasına rağmen aylık maliyeti öngörülebilir tutar. Fiyatlar USD üzerindendir.
Sorun giderme
| Sorun | Sebep | Çözüm |
|---|---|---|
database is locked |
WAL modu kapalıyken eşzamanlı yazma | Bağlantıda PRAGMA journal_mode=WAL ayarlayın |
| Veritabanı dosyası şişiyor | Temizlik rutini kurulmamış | cleanup_old_records() fonksiyonunu günlük çalıştırın |
| Sorgular veri büyüdükçe yavaşlıyor | Eksik indeks | submitted_at ve type sütunlarına indeks ekleyin |
| Önbellekten gelen token reddediliyor | Süresi dolmuş kayıtlar temizlenmemiş | Aramadan önce expires_at filtresini uygulayın |
Sık sorulan sorular
Kaç worker aynı anda bu veritabanına yazabilir?
WAL modunda çok sayıda okuyucu ve tek bir yazıcı aynı anda çalışır. Tek makinede 5–10 worker'a kadar sorunsuz ilerler; busy_timeout değerini 5 saniyenin üzerine çekmek zorunda kalıyorsanız PostgreSQL'e geçiş vakti gelmiştir.
Token TTL'ini kaç saniye seçmeliyim?
Varsayılan 90 saniye güvenli bir başlangıçtır. Formunuz token'ı hemen gönderiyorsa 60 saniyeye indirin; çok adımlı bir akışta bekletiliyorsa TTL'i uzatmak yerine çözümü akışın son adımına taşıyın.
.db dosyasını Git deposuna eklemeli miyim?
Hayır. Dosya token içerir ve her koşuda değişir; .gitignore listesine ekleyin, şemayı ise ayrı bir schema.sql dosyasıyla sürümleyin.
hCaptcha çözümlerini de aynı tabloda takip edebilir miyim?
Tablo tip bağımsızdır, ancak hCaptcha ve FunCaptcha CaptchaAI tarafından desteklenmiyor; GeeTest v4 için de yalnızca "çok yakında" ifadesi geçerli. type sütununda reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3, BLS ve görüntü tabanlı CAPTCHA'ları izleyebilirsiniz. CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) için de aynı şema çalışır.
İlgili makaleler
Sonraki adım
Şemayı kopyalayın, ilk çözümü kaydedin ve ikinci gün ortalama süreyi sorgulayın — CaptchaAI API anahtarınızı alın ve ölçmeye bugün başlayın.
İlgili rehberler: