Playwright betiğiniz sayfayı açıyor, iframe[src*='recaptcha'] selektörünü arıyor ve zaman aşımına uğruyor. Aynı adresin kaynağını curl ile çektiğinizde ne data-sitekey ne de reCAPTCHA betiği var — oysa tarayıcıda widget tam ortada. Sorun selektörünüzde değil: React, Vue veya Angular ile yazılmış bir arayüzde reCAPTCHA ilk HTML'e hiç girmez. Bileşen bağlandığında ya da kullanıcı bir düğmeye dokunduğunda JavaScript tarafından sonradan basılır.
Bu yüzden tek sayfalı uygulamalarda çözüm akışı üç noktada klasik formlardan ayrılır: widget'ı beklemek, sitekey'i sayfa kaynağının dışından okumak ve token'ı bileşen kendini yeniden çizmeden doğru saniyede yerleştirmek. Aşağıdaki dört adım, Python (Playwright) ve Node.js (Puppeteer) örnekleriyle tam olarak bunu yapıyor.
Klasik sayfa ile SPA arasında ne değişir?
| Nerede | Klasik form sayfası | SPA arayüzü |
|---|---|---|
| reCAPTCHA betiği | İlk HTML yanıtında gelir | Rota değişiminden sonra enjekte edilir |
| Widget | Sayfa yüklenirken çizilir | Bileşen bağlandığında (mount) çizilir |
| sitekey | Sayfa kaynağından okunur | JavaScript paketinde veya iframe adresinde |
| Form gönderimi | POST ile tam sayfa yenilemesi | XHR/fetch ile arka planda |
| Token'ın ömrü | Sayfa yenilenene kadar durur | Bileşen yeniden çizilirse anında silinir |
Son satır otomasyonu en çok yanıltan davranıştır: token'ı gizli alana yazarsınız, React state'i günceller, alan boşalır ve istek token'sız gider. Enjeksiyon anı, çözüm süresinden daha belirleyicidir.
Akışın tamamı dört adımdan oluşuyor:
- Widget'ı ortaya çıkaran kullanıcı eylemini taklit edin.
- iframe veya
[data-sitekey]DOM'a girene kadar bekleyin. - sitekey'i çıkarıp CaptchaAI'ye gönderin, sonucu sorgulayın.
- Token'ı formu göndermeden hemen önce enjekte edin ya da callback'i çağırın.
Adım 1: Widget DOM'a girene kadar bekleyin
page.goto() sonrası hemen selektör aramak bir SPA'da neredeyse her zaman erkendir. Önce widget'ı görünür kılan eylemi tekrarlayın — giriş sekmesine tıklamak, sepeti onaylamak, rota değiştirmek — sonra iframe'i bekleyin. 15 saniyelik zaman aşımı yavaş bağlantılar için makul bir üst sınırdır.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://staging.example.com/qa-login")
# SPA may need a click or navigation to trigger reCAPTCHA
page.click("#show-login-form")
# Wait for reCAPTCHA iframe to appear
recaptcha_frame = page.wait_for_selector(
"iframe[src*='recaptcha']",
timeout=15000
)
print("reCAPTCHA detected:", recaptcha_frame.get_attribute("src"))
Geliştirme sırasında headless=False kullanın: widget'ın tam olarak ne zaman çizildiğini kendi gözünüzle görürsünüz.
Adım 2: sitekey'i üç ayrı kaynaktan çıkarın
sitekey bir SPA'da tek bir yerde durmaz; sırayla deneyin. iframe adresindeki k parametresi en güvenilir kaynaktır, [data-sitekey] özniteliği ikinci sıradadır. JavaScript paketini taramak ise son çare olmalı: paket her derlemede değişebilir.
# Method 1: From the iframe src
iframe_src = recaptcha_frame.get_attribute("src")
# src contains: ...?k=6LcR_RsTAAAAADge...
import re
match = re.search(r'[?&]k=([^&]+)', iframe_src)
site_key = match.group(1) if match else None
# Method 2: From data-sitekey attribute
site_key = page.eval_on_selector(
"[data-sitekey]",
"el => el.getAttribute('data-sitekey')"
)
# Method 3: From JavaScript bundle (last resort)
site_key = page.evaluate("""
() => {
// Check for grecaptcha render calls
const scripts = document.querySelectorAll('script');
for (const s of scripts) {
const match = s.textContent.match(/sitekey['":\s]+(['"])(6L[^'"]+)\\1/);
if (match) return match[2];
}
return null;
}
""")
Çıkardığınız 6L... değeri herkese açıktır; gizli bir bilgi değildir.
Adım 3: Çözüm görevini CaptchaAI'ye gönderin
Buradan sonrası SPA'ya özel değil. reCAPTCHA v2 için method=userrecaptcha, googlekey ve pageurl parametreleriyle in.php'ye istek gönderir, ardından res.php'yi sorgularsınız. pageurl alanına kullanıcının o an gördüğü rotayı yazın, ilk açılış adresini değil.
import requests
import time
def solve_recaptcha(site_key, page_url):
# Submit task
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
# Poll for result
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
reCAPTCHA v2 çözümleri 60 saniyenin altında tamamlanır; üç saniyelik sorgulama aralığı gereksiz istek üretmeden yeterlidir. Aynı anda kaç formu test edebileceğinizi plan thread sayınız belirler: BASIC ($15/ay, 5 thread) ile beş çözüm eşzamanlı akışta olabilir, ADVANCE ($90/ay, 50 thread) ile elli. Faturalama thread başınadır ve thread başına çözüm sayısı sınırsızdır.
Adım 4: Token'ı uygulamanın beklediği yere koyun
Klasik bir formda g-recaptcha-response alanını doldurup POST etmek yeterlidir. SPA'da bunun üç yolu var ve hangisinin çalışacağı uygulamanın kendi kodunu nasıl yazdığına bağlı.
Yöntem 1: Gizli textarea'yı doldurun
En kısa yol. Uygulama gönderim anında alanı doğrudan okuyorsa çalışır. Token'ı yazdıktan sonra araya bekleme koymadan formu gönderin.
token = solve_recaptcha(site_key, "https://staging.example.com/qa-login")
# Inject into the g-recaptcha-response textarea
page.evaluate(f"""
document.getElementById('g-recaptcha-response').value = '{token}';
""")
# Submit the form
page.click("#login-button")
Yöntem 2: callback fonksiyonunu elle çağırın
React ve Vue bileşenlerinin çoğu DOM'daki değeri değil, data-callback ile kaydedilen fonksiyonun ilettiği değeri kendi state'ine yazar; böyle bir uygulamada textarea'yı doldurmak hiçbir şey değiştirmez. Ayrıntı için reCAPTCHA v2 callback mekanizması yazısına bakın.
# Find the callback function name
callback = page.evaluate("""
() => {
const widget = document.querySelector('.g-recaptcha');
return widget?.getAttribute('data-callback') || null;
}
""")
# Call it with the token
if callback:
page.evaluate(f"window['{callback}']('{token}')")
else:
# Try the default grecaptcha callback
page.evaluate(f"""
if (window.grecaptcha) {{
// Set the response and trigger verification
document.getElementById('g-recaptcha-response').value = '{token}';
// Find and call any registered callbacks
const form = document.querySelector('form');
if (form) form.dispatchEvent(new Event('submit'));
}}
""")
Yöntem 3: XHR'i dinleyip token'ın gittiğini doğrulayın (Puppeteer)
Form bir REST uç noktasına JSON gönderiyorsa isteği yakalayıp gövdesine bakın: token gerçekten gidiyor mu, yoksa alan boş mu — tek adımda görürsünüz.
const puppeteer = require('puppeteer');
async function solveRecaptchaInSPA() {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// Intercept the API call and inject the token
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/login')) {
const postData = JSON.parse(request.postData() || '{}');
// Token already set via page.evaluate — let it pass
request.continue();
} else {
request.continue();
}
});
await page.goto('https://staging.example.com/qa-login');
await page.waitForSelector('[data-sitekey]');
const siteKey = await page.$eval(
'[data-sitekey]',
el => el.getAttribute('data-sitekey')
);
// Solve with CaptchaAI (implementation omitted — same as Python)
const token = await solveCaptcha(siteKey, page.url());
// Inject token and trigger callback
await page.evaluate((t) => {
document.getElementById('g-recaptcha-response').value = t;
const callback = document.querySelector('.g-recaptcha')
?.getAttribute('data-callback');
if (callback && window[callback]) {
window[callback](t);
}
}, token);
}
Framework'e göre bekleme kalıpları
Üç büyük framework aynı widget'ı farklı zamanlamalarla çizer. Beklediğiniz selektörü buna göre seçin.
React (react-google-recaptcha)
React uygulamaları çoğunlukla react-google-recaptcha paketini kullanır ve bileşen eşzamansız bağlanır. state="attached" ile beklerseniz, öğe henüz görünür olmasa bile DOM'a girdiği anda betiğiniz yoluna devam eder.
# Wait for React to mount the component
page.wait_for_selector(".g-recaptcha", state="attached")
# The sitekey is in the rendered div's data attribute
site_key = page.eval_on_selector(
".g-recaptcha", "el => el.dataset.sitekey"
)
Vue (vue-recaptcha)
Vue tarafında widget genellikle v-if ile koşullu çizilir; koşulu tetiklemeden beklerseniz sonuç zaman aşımıdır.
# Vue may use v-if to conditionally render
# Navigate or interact to trigger the condition
page.click("#proceed-to-checkout")
page.wait_for_selector("iframe[src*='recaptcha']")
Angular
Angular projelerinde reCAPTCHA modülü tembel yüklenebilir; birden fazla olası selektörü tek beklemede toplamak sürüm farklarına dayanıklıdır.
# Angular apps may lazy-load reCAPTCHA modules
# Wait for the specific Angular component
page.wait_for_selector("re-captcha, app-recaptcha, [data-sitekey]")
Rota değiştiğinde widget'ı yeniden yakalayın
SPA'da gezinme tam sayfa yenilemesi yapmaz: URL değişir, sayfa nesnesi aynı kalır. Yeni rotadaki reCAPTCHA'yı yakalamak için goto yerine beklemeyi kullanın.
# Listen for reCAPTCHA appearing after SPA navigation
page.goto("https://example.com")
# Navigate within the SPA
page.click("a[href='/login']")
# The URL changed but no page reload happened
# Wait for the CAPTCHA to render in the new "page"
page.wait_for_selector("iframe[src*='recaptcha']", timeout=10000)
Bir önceki rotadan kalan token'ı yeni formda kullanmayın: yeni rota farklı bir pageurl demektir. Aynı sitede reCAPTCHA ile Turnstile yan yana çalışıyorsa iki akışı ayırın — bu senaryoyu reCAPTCHA v2 ve Turnstile'ın aynı sitede yönetimi yazısında ele alıyoruz.
Yerel senaryo: bir ödeme adımının gece regresyon testi
Türkiye'deki geliştiricilerin sık karşılaştığı SPA senaryolarından biri, Vue veya React ile yazılmış bir ödeme adımıdır: müşteri sepeti onaylar, tr-TR biçiminde adres formu açılır ve reCAPTCHA yalnızca "Siparişi Tamamla" düğmesine basıldıktan sonra çizilir. Europe/Istanbul saatiyle 03.00'te çalışan regresyon paketiniz bu adımda takılırsa sabah gelen rapor "ödeme bozuldu" der; oysa bozulan tek şey widget'ın zamanlamasıdır.
Böyle bir akışı test ederken iki noktaya dikkat edin. Test verisi gerçek müşteri kaydı olmasın: staging veritabanı ve +90 5XX XXX XX XX biçiminde üretilmiş sahte numaralar kullanın — gerçek kişisel veriyle test etmek KVKK kapsamında gereksiz risk yaratır. Maliyet tarafında çözüm başına ücretlendirme, kur dalgalanmasıyla bütçeyi öngörülemez kılar; thread bazlı sabit USD fiyat, test sayısı ikiye katlansa bile aynı kalır.
Sık karşılaşılan hatalar
| Belirti | Sebep | Çözüm |
|---|---|---|
g-recaptcha-response alanı bulunamıyor |
Widget henüz çizilmedi | wait_for_selector'ı yeterli zaman aşımıyla kullanın |
| Token yazıldı ama form gönderilmiyor | Callback tetiklenmedi | data-callback fonksiyonunu bulup çağırın |
| sitekey sayfa kaynağında yok | JS paketinde ya da dinamik olarak yükleniyor | Widget çizilene kadar bekleyip iframe adresinden okuyun |
| Token bir kez çalışıp sonra reddediliyor | Bileşen yeniden çizilirken alanı temizliyor | Token'ı gönderimden hemen önce enjekte edin |
| Enjeksiyondan sonra widget sıfırlanıyor | React/Vue reaktivitesi değeri siliyor | textarea yerine data-callback yöntemine geçin |
| Token hedef sayfada reddediliyor | Rota değişti, pageurl güncellenmedi |
Çözüm isteğine page.url() değerini yazın |
Sık sorulan sorular
Token'ı enjekte ediyorum ama alan hemen boşalıyor, ne yapmalıyım?
Bileşen yeniden çizildiği için oluyor. Token'ı gönderimden hemen önce yazın veya doğrudan data-callback fonksiyonunu çağırın; ikinci yöntem değeri state'e taşıdığından reaktivite onu silmez.
SPA'da pageurl olarak hangi adresi göndermeliyim?
Kullanıcının o an gördüğü rotayı, yani page.url() çıktısını. İlk açılış adresini göndermek token'ın hedef sayfada reddedilmesine yol açar.
Aynı token'ı ikinci bir formda tekrar kullanabilir miyim?
Hayır. reCAPTCHA token'ları tek kullanımlıktır ve Google tarafında yaklaşık iki dakika sonra geçerliliğini yitirir. Her form gönderimi için yeni bir çözüm isteyin.
reCAPTCHA v3 veya Enterprise kullanan bir SPA'da akış değişir mi?
Bekleme ve enjeksiyon mantığı aynı kalır; değişen tek şey gönderdiğiniz parametrelerdir (v3 tarafında action ve beklenen skor). CaptchaAI reCAPTCHA v2, v2 Invisible, v2 Callback, v2/v3 Enterprise ve v3'ü destekler.
CaptchaAI hCaptcha'yı da çözüyor mu?
Hayır. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor; GeeTest v4 için yalnızca "çok yakında" durumu geçerli. Desteklenenler: reCAPTCHA ailesi, Cloudflare Turnstile ve doğrulama akışı, GeeTest v3, görüntü/OCR, grid, BLS; ayrıca CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta).
İlgili makaleler
- API ile reCAPTCHA v2 callback'ini çözme
- reCAPTCHA v2 ve Turnstile'ın aynı sitede yönetimi
- reCAPTCHA v2 callback mekanizması
Sonraki adım
Dinamik yüklenen bir reCAPTCHA'yı ilk kez çözmek beş dakikanızı alır — CaptchaAI API anahtarınızı alın ve yukarıdaki dört adımı kendi SPA'nızda deneyin.