Scraping betiğiniz çalışıyor, sonra hedef site bir CAPTCHA çıkarıyor ve akış duruyor. Çözüm basit: CAPTCHA doğrulamasını CaptchaAI API'sine devredin, HTTP tarafını Node.js'te tutun. Bu rehberde axios ve Cheerio ile sitekey çıkarımından token gönderimine kadar tüm akışı kuruyoruz.
Yerinde bir örnek: Türkiye'deki bir e-ticaret ekibinin kendi fiyat sayfalarını doğrulayan QA testinde form reCAPTCHA ile korunuyorsa betik takılır; aşağıdaki akış bu tür yetkili senaryolarda CAPTCHA'yı otomatik çözer. Kazınan veride kişisel bilgi varsa KVKK kapsamına girdiğini unutmayın ve çalışmayı yalnızca izinli akışlarla sınırlayın.
Başlamadan önce: gereksinimler
| Gereksinim | Ayrıntı |
|---|---|
| Node.js 16+ | npm ile birlikte |
| axios | npm install axios |
| cheerio | npm install cheerio |
| CaptchaAI API anahtarı | captchaai.com üzerinden alın |
CaptchaAI çözücü modülü
// captcha-solver.js
const axios = require("axios");
class CaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = "https://ocr.captchaai.com";
}
async _submit(params) {
params.key = this.apiKey;
const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (!resp.data.startsWith("OK|")) {
throw new Error(`Submit error: ${resp.data}`);
}
return resp.data.split("|")[1];
}
async _poll(taskId, timeout = 300000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const resp = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (resp.data === "CAPCHA_NOT_READY") continue;
if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
throw new Error(`Solve error: ${resp.data}`);
}
throw new Error("Solve timed out");
}
async solveRecaptchaV2(siteKey, pageUrl) {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
version: "v3",
action,
});
return this._poll(taskId);
}
async solveTurnstile(siteKey, pageUrl) {
const taskId = await this._submit({
method: "turnstile",
sitekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
}
module.exports = CaptchaSolver;
Modül görevi in.php'ye gönderir, sonucu res.php üzerinden sorgular ve token'ı döndürür. _poll her beş saniyede bir sonucu sorgular ve varsayılan olarak 300 saniye sonra zaman aşımına düşer; OCR veya grid gibi daha yavaş türlerde bu değeri artırabilirsiniz. _submit yanıtı OK| ile başlamıyorsa hata fırlatır, böylece yanlış sitekey ya da bitmiş bakiye gibi sorunları sessizce yutmak yerine erken görürsünüz.
reCAPTCHA v3 ve Turnstile için aynı çözücü
Çözücü modül üç yöntemi bir arada sunar; hangisini çağıracağınız hedef sitenin kullandığı CAPTCHA türüne bağlıdır:
- reCAPTCHA v2 — görünür ya da görünmez onay kutusu;
solveRecaptchaV2(siteKey, pageUrl)çağırın ve dönen token'ı formdakig-recaptcha-responsealanına yazın. - reCAPTCHA v3 — puan tabanlı, tamamen görünmez;
solveRecaptchaV3(siteKey, pageUrl, action)kullanın.actiondeğeri sayfadakigrecaptcha.executeçağrısındaki değerle birebir eşleşmelidir, aksi halde dönen token düşük puan alır. - Cloudflare Turnstile —
solveTurnstile(siteKey, pageUrl); dönen token, Turnstile formundakicf-turnstile-responsealanına yazılır.
Üç yöntem de aynı gönder–sorgula–döndür döngüsünü paylaşır; yalnızca in.php'ye giden method ve parametre adları değişir. Bu yüzden yeni bir tür eklemek genellikle tek bir küçük metot yazmak kadar basittir.
reCAPTCHA korumalı bir sayfayı kazıma
const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");
const solver = new CaptchaSolver("YOUR_API_KEY");
async function scrapeProtectedPage(url) {
// Step 1: Load the page
const { data: html } = await axios.get(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
const $ = cheerio.load(html);
// Step 2: Extract site key
const siteKey = $(".g-recaptcha").attr("data-sitekey");
if (!siteKey) {
console.log("No CAPTCHA found, page loaded directly");
return html;
}
console.log("Site key found:", siteKey);
// Step 3: Solve the CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
console.log("Token received:", token.substring(0, 50));
// Step 4: Submit with the token
const result = await axios.post(
url,
new URLSearchParams({
"g-recaptcha-response": token,
q: "search query",
}),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
return result.data;
}
Birden fazla sayfayı eş zamanlı kazıma
async function scrapePages(urls, siteKey, concurrency = 3) {
const results = [];
const queue = [...urls];
const worker = async () => {
while (queue.length > 0) {
const url = queue.shift();
try {
const token = await solver.solveRecaptchaV2(siteKey, url);
const { data } = await axios.post(
url,
new URLSearchParams({ "g-recaptcha-response": token }),
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
results.push({ url, data, success: true });
console.log(`Scraped: ${url}`);
} catch (err) {
results.push({ url, error: err.message, success: false });
console.error(`Failed: ${url} - ${err.message}`);
}
}
};
// Run workers concurrently
const workers = Array(concurrency)
.fill(null)
.map(() => worker());
await Promise.all(workers);
return results;
}
// Usage
const urls = [
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);
Buradaki concurrency değeri, CaptchaAI planınızdaki thread sayısıyla doğrudan ilişkilidir. Fatura çözüm başına değil, eş zamanlı thread başına kesilir: BASIC planı ($15/ay, 5 thread) aynı anda 5 CAPTCHA'yı işler. concurrency'yi thread sayınızla sınırlı tutun; daha yüksek bir değer, kuyruğu hızlandırmak yerine yalnızca boşta bekleyen istekler üretir.
Hata yönetimi ve yeniden deneme
Yukarıdaki worker döngüsü her URL'yi ayrı bir try/catch içine alır, böylece tek bir başarısız istek toplu işi durdurmaz; hata mesajı sonuç dizisine yazılır ve döngü kuyruktaki sıradaki URL ile devam eder. Üretim iş yüklerinde bunun üzerine iki katman daha ekleyin:
- Geçici hatalar için yeniden deneme.
ECONNREFUSEDveya HTTP 5xx gibi geçici hatalarda isteği hemen tekrarlamak yerine üstel geri çekilme (exponential backoff) uygulayın: bekleme süresini her denemede iki katına çıkarın ve üst sınır koyun. - Kalıcı hataları ayırın. Yanlış
sitekeyya da geçersiz API anahtarı gibi kalıcı hataları yeniden denemeyin; bunlar her seferinde aynı sonucu verir ve yalnızca bakiye tüketir._submitfırlattığı hata mesajını kaydedip o URL'yi atlayın.
Sonuç dizisindeki success: false kayıtlarını ayrı bir kuyruğa toplayıp çalışmanın sonunda tek seferde raporlamak, hangi sayfaların neden düştüğünü görmeyi kolaylaştırır.
Oturum ve çerez yönetimi
Oturum çerezi bekleyen sitelerde çerezleri taşımak için axios'u çerez kavanozuyla sarmalayın:
const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");
const jar = new CookieJar();
const client = wrapper(
axios.create({
jar,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
})
);
async function scrapeWithSession(url, siteKey) {
// Initial page load sets cookies
await client.get(url);
// Solve CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
// Submit with maintained cookies
const result = await client.post(
url,
new URLSearchParams({ "g-recaptcha-response": token })
);
return result.data;
}
İlk GET isteği çerezleri kavanoza yazar; aynı istemciyle POST ettiğinizde oturum korunur.
Sonuçları Cheerio ile ayrıştırma
function parseResults(html) {
const $ = cheerio.load(html);
const items = [];
$(".result-item").each((_, el) => {
items.push({
title: $(el).find(".title").text().trim(),
url: $(el).find("a").attr("href"),
description: $(el).find(".description").text().trim(),
});
});
return items;
}
Cheerio, jQuery benzeri seçicilerle HTML'i ayrıştırır. Seçiciler boş dönüyorsa içerik büyük olasılıkla JavaScript ile oluşturuluyordur; bu durumda sunucudan gelen ham HTML yerine tarayıcı tarafından render edilmiş DOM'a ihtiyacınız vardır ve Puppeteer'a geçmeniz gerekir.
İstek sınırlama ve yasal uyum
Eş zamanlı çözüm hızı arttıkça hedef sunucuya giden istek yoğunluğu da artar. Sorumlu bir kazıma akışı için birkaç kuralı baştan yerleştirin:
- İstekler arasına makul bir gecikme koyun ve toplam hızı hedef sitenin kaldırabileceği seviyede tutun; amaç veriyi toplamaktır, sunucuyu zorlamak değil.
- Yalnızca erişim izniniz olan ya da sahibi olduğunuz sayfaları kazıyın;
robots.txtve kullanım şartlarını kontrol edin. - Kazınan veride ad, e-posta, telefon gibi kişisel bilgiler varsa bu veri KVKK kapsamına girer. Toplama amacını sınırlayın, gereğinden fazla veri saklamayın ve çalışmayı yetkili QA veya veri toplama akışlarıyla sınırlı tutun.
Bu sınırlar hem hedef siteyle ilişkinizi korur hem de betiğinizin ECONNREFUSED ya da kalıcı 403 yanıtlarıyla engellenme olasılığını düşürür.
Sorun giderme
| Sorun | Olası neden | Çözüm |
|---|---|---|
CAPTCHA_NOT_READY sonsuz döngüde |
Yanlış sitekey veya yavaş çözüm |
sitekey değerini doğrulayın; zaman aşımı süresini artırın |
POST isteğinde 403 Forbidden |
Eksik çerez veya başlık | Oturum çerezlerini taşıyın; Referer başlığı ekleyin |
| Cheerio öğeleri bulamıyor | İçerik JavaScript ile oluşturuluyor | JS ile render edilen siteler için Puppeteer kullanın |
ECONNREFUSED |
Hedef site istek sınırlaması uyguluyor | İstekler arasına gecikme koyun; proxy yapılandırmasını gözden geçirin |
Sık sorulan sorular
axios yerine ne zaman Puppeteer kullanmalıyım?
Hedef site standart form gönderimiyle düz HTML döndürüyorsa axios + Cheerio en hafif seçenektir. Sayfa JavaScript çalıştırıyor ya da dinamik içerik gerektiriyorsa Puppeteer kullanın.
CaptchaAI hangi CAPTCHA türlerini çözer?
reCAPTCHA v2/v3, Cloudflare Turnstile ve Challenge, GeeTest v3, görüntü/OCR, grid ve BLS türlerini çözer; CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) beta aşamasındadır. hCaptcha ve FunCaptcha ise henüz desteklenmiyor.
Fiyatlandırma çözüm başına mı, yoksa thread bazlı mı?
Thread bazlı. Her plan, eş zamanlı thread sayısı kadar CAPTCHA'yı aynı anda işler ve fatura ayı boyunca thread başına sınırsız çözüm sunar. Çözüm başına ücret yoktur.
Token geçerli olduğu halde neden 403 alıyorum?
Genellikle eksik oturum çerezi ya da başlık yüzündendir. Sayfayı önce GET ile yükleyip çerezleri saklayın ve token'ı süresi dolmadan gönderin.
Cloudflare korumalı siteleri nasıl kazırım?
Site Turnstile kullanıyorsa solver.solveTurnstile() yeterlidir. qa_session_cookie çerezini döndüren tam Cloudflare doğrulama akışı için Cloudflare çözüm rehberine bakın.