Paralel tarayıcı otomasyonunda darboğaz genellikle Chrome değildir; her oturumun CAPTCHA doğrulaması için beklediği ölü süredir. Tek makinede beş oturum çalıştırıp her birinde 15–30 saniye beklerseniz, iş yükünüzün büyük bölümünü CPU değil bekleme tüketir. Selenium Grid bu beklemeyi yatayda dağıtır: oturumlar birden fazla düğüme yayılır, CAPTCHA çözümü ise tek bir CaptchaAI API anahtarı üzerinden merkezî olarak yürür.
Bu rehber üretimde çalışan bir kurulumun tüm parçalarını verir: Docker Compose ile Selenium 4 Grid, düğüm başına oturum ayarları, Python ve Java istemcileri, kapasite izleme, Kubernetes ile otomatik ölçeklendirme ve sık karşılaşılan hataların çözümü.
Önce kapasite: kaç düğüm, kaç thread?
Grid'i kurmadan önce iki sayıyı birbirinden ayırın; bunlar birbirinin karşılığı değildir:
- Tarayıcı eşzamanlılığı: Grid'de aynı anda açık olan Chrome oturumu sayısı. Düğüm başına 3–5 oturum makul bir başlangıçtır ve pratikte RAM ile sınırlıdır (oturum başına yaklaşık 1–2 GB ayırın).
- CaptchaAI thread sayısı: aynı anda API'ye gönderebileceğiniz çözüm sayısı. CaptchaAI thread başına ücretlendirir ve her planda thread başına sınırsız çözüm vardır — çözüm adedi değil, eşzamanlılık faturalanır.
Thread bütçeniz, tepe anında CAPTCHA bekleyen oturum sayısına eşit olmalıdır. Grid boyutunu plana çevirmek şu kadar basit:
| Grid boyutu | Tepe eşzamanlılık | Uygun plan |
|---|---|---|
| 1 düğüm × 5 oturum | 5 | BASIC ($15/ay, 5 thread) |
| 3 düğüm × 5 oturum | 15 | STANDARD ($30/ay, 15 thread) |
| 10 düğüm × 5 oturum | 50 | ADVANCE ($90/ay, 50 thread) |
| 20 replika × 3 oturum | 60 | PREMIUM ($170/ay, 100 thread) |
Bu ayrımın Türkiye'deki ekipler için pratik bir sonucu var. İstanbul'daki bir e-ticaret ekibinin gece çalışan ödeme adımı regresyon paketini düşünün: paket Europe/Istanbul saatiyle 02:00'de tetikleniyor ve 120 senaryonun 40'ı CAPTCHA korumalı bir formdan geçiyor. Ekip Grid'i geceleri 6 düğüme ölçekliyor, gündüz 2 düğümde bırakıyor. Fatura tarafında değişen bir şey yok: thread planı aylık ve USD sabit olduğu için kur dalgalanmasına bağlı bir birim maliyet hesabı yapmanız gerekmiyor. Çözüm başına ücretlendirmede aynı gece paketi ay sonunda öngörülemeyen bir kalem olurdu.
Mimari: tek anahtar, çok düğüm
Grid Hub istekleri yönlendirir, düğümler tarayıcıyı çalıştırır, CAPTCHA çözümü ise düğümden bağımsız bir HTTP çağrısıdır. Bu yüzden her düğüme ayrı anahtar dağıtmanız gerekmez: aynı API anahtarı tüm düğümlerde kullanılır ve eşzamanlılık plan tarafında yönetilir.
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Test Script │────▶│ Grid Hub │────▶│ Node 1 │
│ (Client) │ │ (Router) │ │ Chrome x 5 │
└─────────────┘ └──────────────┘ └──────────────┘
│ ┌──────────────┐
├─────────────▶│ Node 2 │
│ │ Chrome x 5 │
│ └──────────────┘
│ ┌──────────────┐
└─────────────▶│ Node 3 │
│ Chrome x 5 │
└──────────────┘
All nodes share ──▶ CaptchaAI API (single API key)
Adım 1: Docker Compose ile Selenium Grid 4
Hub ve üç Chrome düğümü için minimum yapılandırma aşağıda. SE_NODE_MAX_SESSIONS düğüm başına oturum sayısını belirler; SE_NODE_OVERRIDE_MAX_SESSIONS ise Selenium'un CPU çekirdek sayısına göre koyduğu varsayılan sınırın aşılmasını sağlar.
version: "3"
services:
selenium-hub:
image: selenium/hub:4.21.0
container_name: selenium-hub
ports:
- "4442:4442"
- "4443:4443"
- "4444:4444"
chrome-node-1:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-2:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
chrome-node-3:
image: selenium/node-chrome:4.21.0
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
docker-compose up -d
Servisler ayağa kalktığında http://localhost:4444 adresindeki Grid konsolunda üç düğümü ve toplam 15 yuvayı görmeniz gerekir. Yuva sayısı beklediğinizden düşükse neredeyse her zaman SE_NODE_OVERRIDE_MAX_SESSIONS unutulmuştur.
Adım 2: Grid için CaptchaAI istemcisi
İstemci iki işi ayrı tutar: uzak oturum açmak ve çözümü API'den almak. create_session Hub'a bağlanır, solve_recaptcha_v2 ve solve_turnstile görevi in.php uç noktasına gönderip sonucu res.php üzerinden periyodik olarak sorgular, process_task ise bir görevin tamamını tek düğüm üzerinde yürütür: sitekey tespiti, çözüm, token enjeksiyonu, form gönderimi.
Token'ı sayfaya yazarken alan adı kritiktir: reCAPTCHA v2 için
g-recaptcha-response, Cloudflare Turnstile içincf-turnstile-response. Enjeksiyondan sonra formu gecikmeden gönderin; token'ın geçerlilik süresi kısadır ve Grid'de sıraya giren bir oturum bu süreyi kolayca tüketir.
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from concurrent.futures import ThreadPoolExecutor, as_completed
class GridCaptchaSolver:
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, grid_url="http://localhost:4444"):
self.api_key = api_key
self.grid_url = grid_url
def create_session(self):
"""Create a new browser session on the Grid."""
options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_argument("--no-sandbox")
options.add_argument("--window-size=1920,1080")
driver = webdriver.Remote(
command_executor=self.grid_url,
options=options,
)
return driver
def solve_recaptcha_v2(self, site_url, sitekey):
"""Solve reCAPTCHA v2 via CaptchaAI API."""
# Submit
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": site_url,
"json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
# Poll
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def solve_turnstile(self, site_url, sitekey):
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key, "method": "turnstile",
"sitekey": sitekey, "pageurl": site_url, "json": 1,
})
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_URL}/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def process_task(self, task):
"""Process a single CAPTCHA-protected task on a Grid node."""
driver = self.create_session()
try:
driver.get(task["url"])
time.sleep(2)
# Detect sitekey
sitekey = task.get("sitekey")
if not sitekey:
sitekey = driver.execute_script(
"return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)
if not sitekey:
return {"url": task["url"], "status": "no_captcha", "data": driver.page_source[:500]}
# Solve
token = self.solve_recaptcha_v2(task["url"], sitekey)
# Inject
driver.execute_script(f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(
el => el.value = '{token}'
);
""")
# Fill form and submit
if task.get("form_data"):
for field, value in task["form_data"].items():
driver.find_element(By.NAME, field).send_keys(value)
if task.get("submit_selector"):
driver.find_element(By.CSS_SELECTOR, task["submit_selector"]).click()
time.sleep(3)
return {
"url": task["url"],
"status": "success",
"result_url": driver.current_url,
"data": driver.page_source[:1000],
}
except Exception as e:
return {"url": task["url"], "status": "error", "error": str(e)}
finally:
driver.quit()
Aynı sınıfa Turnstile için ayrı bir metot koymak, method parametresi dışında hiçbir şeyin değişmediğini görünür kılar: gönderim, sorgulama ve hata yönetimi ortaktır. Yeni bir CAPTCHA türü eklerken tek yapmanız gereken doğru parametre setini geçirmektir.
Adım 3: Görevleri paralel çalıştırın
ThreadPoolExecutor ile görev havuzunu Grid'e dağıtın. max_workers değerini Grid'in boş yuva sayısının üzerine çıkarmayın: fazla worker Hub'da kuyruğa girer ve SessionNotCreated hatasına dönüşür. Yuva sayısı ile thread sayısını da eşleştirin — 15 yuvaya karşılık 5 thread'lik bir plan tarayıcıları boşta bekletir.
def run_parallel_tasks(api_key, tasks, max_workers=10):
"""Run CAPTCHA tasks in parallel across Grid nodes."""
solver = GridCaptchaSolver(api_key)
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(solver.process_task, task): task
for task in tasks
}
for future in as_completed(futures):
task = futures[future]
try:
result = future.result(timeout=600)
results.append(result)
print(f"[{result['status']}] {result['url']}")
except Exception as e:
results.append({
"url": task["url"],
"status": "exception",
"error": str(e),
})
return results
# Usage
tasks = [
{
"url": "https://site-a.com/form",
"sitekey": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"form_data": {"name": "Test User", "email": "test@example.com"},
"submit_selector": "#submit",
},
{
"url": "https://site-b.com/register",
"sitekey": "6LdKlZEpAAAAAAOQjzC2v_mJ-",
"form_data": {"username": "testuser"},
"submit_selector": "button[type='submit']",
},
# Add more tasks...
]
results = run_parallel_tasks("YOUR_API_KEY", tasks, max_workers=15)
# Summary
success = sum(1 for r in results if r["status"] == "success")
print(f"\nCompleted: {success}/{len(results)} successful")
as_completed kullanımı burada önemli: sonuçlar tamamlanma sırasına göre toplanır, böylece tek bir yavaş görev tüm turu bloke etmez. Her görev için konulan future.result(timeout=600) üst sınırı da kilitlenen bir oturumun havuzu tutmasını engeller.
Adım 4: Grid kapasitesini izleyin
Sabit bir max_workers değeri, düğüm sayısı değiştiği anda yanlış olur. Grid'in /status uç noktası boş yuva sayısını verir; havuz boyutunu her turdan önce buradan hesaplayın.
import requests
def check_grid_status(grid_url="http://localhost:4444"):
"""Check Selenium Grid status and available nodes."""
try:
resp = requests.get(f"{grid_url}/status")
data = resp.json()
nodes = data.get("value", {}).get("nodes", [])
total_slots = 0
available_slots = 0
print(f"Grid Status: {data['value']['ready']}")
print(f"Nodes: {len(nodes)}")
for i, node in enumerate(nodes):
slots = node.get("slots", [])
free = sum(1 for s in slots if not s.get("session"))
total_slots += len(slots)
available_slots += free
print(f" Node {i+1}: {free}/{len(slots)} slots available")
print(f"Total capacity: {available_slots}/{total_slots} available")
return available_slots
except Exception as e:
print(f"Grid check failed: {e}")
return 0
# Adjust workers based on grid capacity
available = check_grid_status()
optimal_workers = min(available, 20)
print(f"Optimal workers: {optimal_workers}")
Bu kontrolü CI iş akışınızın en başında çalıştırın; iki tipik hatayı birden eler:
- Düğümler henüz hazır değilken başlayan test paketi.
- Küçülmüş bir Grid'e eski
max_workersdeğeriyle gönderilen tur.
Sorun giderme
Ölçeği büyütmeden önce bu tabloyu geçin: aşağıdaki altı hata tek düğümde de görülür ve düğüm sayısını artırmak hiçbirini çözmez, yalnızca çoğaltır.
| Sorun | Sebep | Düzeltme |
|---|---|---|
SessionNotCreated |
Boş yuva kalmadı | Düğüm sayısını veya SE_NODE_MAX_SESSIONS değerini artırın |
| Grid'de zaman aşımı | Düğüm aşırı yüklendi | Düğüm başına eşzamanlı oturum sayısını azaltın |
WebDriverException |
Düğüm bağlantısı koptu | Oturum oluşturmaya yeniden deneme mantığı ekleyin |
| Bellek tükenmesi | Çok fazla tarayıcı örneği | Kaynak sınırlarını ve maksimum oturum sayısını ayarlayın |
| Çözüm zaman aşımı | Yoğun anda istekler sıraya girdi | Sorgulama zaman aşımını uzatın, yeniden deneme ekleyin |
| Eski oturumlar birikiyor | Grid temizleme gecikmesi | SE_SESSION_TIMEOUT değerini düşürün |
Kubernetes ile otomatik ölçeklendirme
Yük gün içinde dalgalanıyorsa düğüm sayısını sabit tutmak yerine HorizontalPodAutoscaler'a bırakın. Aşağıdaki tanım Chrome düğümlerini CPU kullanımına göre 2 ile 20 replika arasında ölçekler; düğüm başına oturum sayısı 3'e düşürülerek bellek baskısı azaltılmıştır.
# selenium-grid-k8s.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: selenium-chrome-node
spec:
replicas: 5
selector:
matchLabels:
app: selenium-chrome
template:
metadata:
labels:
app: selenium-chrome
spec:
containers:
- name: chrome
image: selenium/node-chrome:4.21.0
env:
- name: SE_EVENT_BUS_HOST
value: selenium-hub
- name: SE_EVENT_BUS_PUBLISH_PORT
value: "4442"
- name: SE_EVENT_BUS_SUBSCRIBE_PORT
value: "4443"
- name: SE_NODE_MAX_SESSIONS
value: "3"
resources:
limits:
memory: "2Gi"
cpu: "1"
requests:
memory: "1Gi"
cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: chrome-node-hpa
spec:
scaleRef:
apiVersion: apps/v1
kind: Deployment
name: selenium-chrome-node
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Ölçek tavanınızı belirlerken thread bütçenizi hatırlayın: 20 replika × 3 oturum, 60 eşzamanlı oturum demektir. Bu sayı planınızın thread sayısını aşarsa fazladan gelen istekler kuyrukta bekler ve ölçeklendirmenin kazandırdığı süre orada geri gider. Kararı iki tarafta birlikte alın.
Java tarafında aynı desen
Grid'i Java ile süren ekipler için istemci deseni birebir aynıdır: RemoteWebDriver ile Hub'a bağlanın, çözüm çağrılarını HttpClient üzerinden yapın, paralelliği ExecutorService ile yönetin. newFixedThreadPool boyutunu yine boş yuva sayısı ile thread bütçesinden küçük olanına göre seçin.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.net.URL;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.*;
public class GridCaptchaSolver {
private final String apiKey;
private final String gridUrl;
private final HttpClient httpClient;
public GridCaptchaSolver(String apiKey, String gridUrl) {
this.apiKey = apiKey;
this.gridUrl = gridUrl;
this.httpClient = HttpClient.newHttpClient();
}
public WebDriver createSession() throws Exception {
ChromeOptions options = new ChromeOptions();
options.addArguments("--no-sandbox", "--window-size=1920,1080");
return new RemoteWebDriver(new URL(gridUrl), options);
}
public List<Map<String, String>> runParallel(
List<Map<String, String>> tasks, int workers
) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(workers);
List<Future<Map<String, String>>> futures = new ArrayList<>();
for (Map<String, String> task : tasks) {
futures.add(executor.submit(() -> processTask(task)));
}
List<Map<String, String>> results = new ArrayList<>();
for (Future<Map<String, String>> future : futures) {
results.add(future.get(600, TimeUnit.SECONDS));
}
executor.shutdown();
return results;
}
}
Java tarafında da thread bütçesi aynı kuralla belirlenir; iki çalışma zamanını tek bir Grid'de karıştırıyorsanız toplam eşzamanlılığı ortak hesaplayın.
Sık sorulan sorular
Grid'deki oturum sayısı ile plan thread sayısı aynı şey mi?
Hayır. Oturum sayısı tarayıcı kapasitenizi, thread sayısı aynı anda API'ye gönderebileceğiniz çözüm sayısını belirler. Tepe anında CAPTCHA bekleyen oturum sayısını referans alın: 15 eşzamanlı form için STANDARD ($30/ay, 15 thread) yeterlidir.
Her düğüme ayrı bir API anahtarı tanımlamalı mıyım?
Hayır, tek anahtar tüm düğümlerde kullanılır. Eşzamanlılık anahtar başına değil plan başına yönetildiği için düğüm eklemek anahtar yönetiminizi değiştirmez.
Token'ı aldıktan sonra neden reddediliyor?
En yaygın neden gecikmedir: token enjekte edildikten sonra form geç gönderilirse süre dolar. İkincisi alan adı karışıklığıdır — reCAPTCHA v2 için g-recaptcha-response, Turnstile için cf-turnstile-response alanına yazdığınızdan emin olun.
CaptchaAI hCaptcha'yı da çözüyor mu?
Hayır. Desteklenen türler reCAPTCHA v2/v3, Cloudflare Turnstile ve Cloudflare doğrulama akışı, GeeTest v3 ile görüntü/OCR CAPTCHA'larıdır. hCaptcha ve FunCaptcha desteklenmiyor, GeeTest v4 için çok yakında planı var. CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) beta aşamasındadır.
Topladığım veriler KVKK kapsamına girer mi?
Veri kişisel veri içeriyorsa evet. CAPTCHA çözümü teknik bir adımdır; hukuki sorumluluk toplanan verinin kendisindedir. Yetkiniz olan kaynaklarda, QA ve kendi sistemlerinizin testi çerçevesinde çalışın.
İlgili rehberler
- Python ile Selenium ve CaptchaAI entegrasyonu
- Selenium Wire ile istek yakalama
- Node.js worker thread'leriyle paralel çözüm
Dağıtık tarayıcı altyapınızda çözüm kapasitesini büyütün — CaptchaAI API anahtarınızı alın ve Grid'inize tek anahtarla bağlayın.