Symfony Panther’da reCAPTCHA Nasıl Çözülür (PHP SDK)

Symfony Panther’da CAPTCHA çözmek için sitekey’i Panther’ın açık tuttuğu sayfadan okuyun ve geçerli URL ile birlikte CapSkip PHP SDK’sının recaptcha metoduna verin. Ardından token’ı executeScript ile gizli g-recaptcha-response textarea’sına yazın ve formun kendi düğmesine tıklayarak gönderin. Panther kodu genellikle bu yazma adımında bozulur. reCAPTCHA bu textarea’yı display:none durumunda tutar; bu yüzden onu doldurabilecek her Panther yardımcısı, Form nesnesi de sendKeys de, ElementNotInteractableException fırlatır. Bir reCAPTCHA callback’inden gönderim yapan sayfalar bir adım daha gerektirir, çünkü callback çalışana kadar alanı yok sayarlar. Bu rehber Windows’taki kurulumu, çözümü, yazmayı, iki gönderim türünü ve çalıştırmayı güvenilir kılan beklemeleri ele alıyor.
Neye ihtiyacınız var
- Bir Windows makinesinde çalışan CapSkip. Local modunda 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir; Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki Panther onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.
- php.ini dosyasında curl ve zip eklentileri açık olan PHP 8.1 veya üzeri. Panther 2.4 için PHP 8.1 gerekir, Composer zip olmadan php-webdriver’ı kurmaz ve PHP’nin Windows derlemelerinde iki eklenti de kapalı gelir.
- Chrome ve onunla aynı ana sürüme sahip bir chromedriver. Aşağıdaki bdi aracı doğru olanı indirir.
- Onay kutulu ya da invisible bir reCAPTCHA v2 widget’ı olan bir sayfa.
# PHP 8.1+; bdi fetches the chromedriver that matches your Chrome composer require symfony/panther capskip/capskip composer require --dev dbrekelmans/bdi vendor\bin\bdi detect drivers
Son satır komutun Windows biçimidir; Linux veya macOS’ta vendor/bin/bdi olur. Komut chromedriver.exe dosyasını projenizin içindeki bir drivers klasörüne kaydeder. Panther bu klasöre kendiliğinden bakar, ama Windows’ta bu arama bir tuzaktır. Dosyayı bulur, ardından onu göreli yoldan başlatamaz. Hata "Could not start chrome" şeklindedir ve ardından bir komutun tanınmadığını söyleyen bir cmd.exe mesajı gelir. Buradaki her örnekte olduğu gibi driver’ın tam yolunu createChromeClient’a verin ya da drivers klasörünü PATH’e ekleyin.
1. Adım: sayfayı açın ve sitekey’i okuyun
Panther, chromedriver aracılığıyla headless bir Chrome başlatır ve sayfayı yükler. Ardından reCAPTCHA’nın api.js dosyası widget’ı oluşturur ve id’si g-recaptcha-response olan bir textarea ekler; bu yüzden bu öğeyi beklemek, widget’ın hazır olduğunun güvenilir işaretidir. Öğenin görünür olmasını beklemeyin: hiçbir zaman görünür olmaz ve bu öğe üzerinde çağrılan waitForVisibility zaman aşımına uğrar.
<?php
// composer require symfony/panther capskip/capskip
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Panther\Client;
// The full path: on Windows, Panther finds drivers\chromedriver.exe on
// its own but then fails to start it from the relative path.
$client = Client::createChromeClient(__DIR__ . '/drivers/chromedriver.exe');
$client->request('GET', 'https://example.com/signup');
// api.js adds the hidden textarea once the widget has rendered.
$client->waitFor('#g-recaptcha-response', 20);
$crawler = $client->getCrawler();
$sitekey = $crawler->filter('[data-sitekey]')->attr('data-sitekey');Çoğu sayfa widget’ı data-sitekey özniteliği taşıyan bir div ile tanımlar. Widget’ı kendi script’inden grecaptcha.render ile oluşturan bir sayfada böyle bir öznitelik yoktur, ama anahtar yine de sayfadadır: widget’ın iframe’inin adresinde. Bu sayfalarda 1. adımın son satırı yerine şu bloğu kullanın:
// grecaptcha.render() pages have no data-sitekey; the key is the
// k= parameter of the widget's iframe.
$src = $crawler->filter('iframe[title="reCAPTCHA"]')->attr('src');
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$sitekey = $query['k'] ?? '';2. Adım: PHP SDK ile çözün
CapSkip PHP SDK’sı, sitekey’e ve widget’ın çalıştığı sayfanın URL’sine ihtiyaç duyar. URL’yi kendi değişkeninizden değil, Panther’dan alın: bir yönlendirmeden sonra token’ın üretilmesi gereken sayfa, Panther’ın açık tuttuğu sayfadır.
use CapSkip\CapSkip; $solver = new CapSkip(['host' => '127.0.0.1', 'port' => 8080]); // The page Panther has open, after any redirect, so the token is minted // for the URL the form really lives on. $result = $solver->recaptcha($sitekey, $client->getCurrentURL()); $token = $result['code']; // the result holds captchaId and code
Invisible bir widget iki şekilde karşınıza çıkar: data-size özniteliği invisible olarak ayarlanmış bir div ya da gönder düğmesinin kendisinde bir data-sitekey. Her iki durumda da üçüncü argüman olarak invisible değeri 1 olan bir seçenekler dizisi verin. Google’ın herkese açık test sitekey’inin gerçek bir CapSkip üzerinden yapılan deneme çözümü yaklaşık 13 saniye sürdü ve 1.678 karakterlik bir token döndürdü.
PHP SDK’sı senkron çalışır, bu yüzden betik token geri gelene kadar bloke olur ve bunda bir sakınca yoktur. Panther’ın tarayıcısı sayfada öylece bekler, çünkü php-webdriver’ın zaman aşımları komutlar arasındaki boşluklara değil, her bir komuta uygulanır. 103 saniyelik bir çözüm boyunca açık tutulan bir oturum, sonrasında token’ı yine normal şekilde enjekte edip formu gönderdi. SDK, varsayılanı 300 saniye olan recaptchaTimeout süresine kadar yoklama yapar; seçenekler dizisindeki bir timeout anahtarı bunu tek bir çağrı için değiştirir. Asıl süresi dolan şey token’dır. Bir reCAPTCHA token’ı verildikten sonra yaklaşık iki dakika geçerlidir, bu yüzden onu hemen yazın ve gönderin.
3. Adım: token’ı executeScript ile yazın
Bir Symfony Panther CAPTCHA çalıştırmasının işe yarayıp yaramayacağına karar veren adım budur. Akla ilk gelen hamle alanı Panther’ın Form nesnesi üzerinden ayarlamaktır ve bu başarısız olur. Panther alanları bir kullanıcının yapacağı gibi doldurur: WebDriver aracılığıyla önce temizler, sonra içine yazar. WebDriver ise kimsenin göremediği bir öğeye yazmayı reddeder. Alanı Form üzerinde ayarlamak, setValues çağırmak, alanı submit’e vermek ya da öğe üzerinde sendKeys çağırmak; hepsi aynı ElementNotInteractableException ile sonuçlanır. Bunun yerine değeri sayfanın içinde ayarlayın:
// Throws ElementNotInteractableException: the textarea is display:none.
// $form['g-recaptcha-response'] = $token;
$client->executeScript(
'document.getElementById("g-recaptcha-response").value = arguments[0];',
[$token]
);executeScript, PHP dizisinin her öğesini script’e arguments[0], arguments[1] vb. olarak iletir; bu yüzden token’ı hiçbir zaman kaçış karakterleriyle JavaScript dizesinin içine gömmeniz gerekmez. E-posta adresi gibi görünür alanlar ise yine Panther’ın normal form API’siyle doldurulur.
4. Adım: gerçek düğmeye tıklayarak gönderin
Nasıl gönderdiğiniz, sayfanın ne gördüğünü değiştirir. Formu düğmesinden oluşturun; Panther da onu bir ziyaretçinin yaptığı gibi o düğmeye tıklayarak gönderir:
// Built from its button, the form is sent by clicking that button, so the
// page's own submit handlers and HTML5 validation run as for a visitor.
$form = $client->getCrawler()->selectButton('Sign up')->form();
$form['email'] = 'YOUR_EMAIL';
$client->submit($form);
// Wait for something only the next page has; getCrawler() can lag.
$client->waitFor('#welcome', 15);
echo $client->getTitle(), PHP_EOL;Diğer gönderme yolları bazı şeyleri atlar; bu da kendi script’i gönderim sırasında bir alan ekleyen ya da formu kontrol eden sayfalarda önem taşır:
| Formun gönderilme şekli | Sayfanın submit işleyicileri çalışır | HTML5 doğrulaması | Düğme adı ve değeri gönderilir |
|---|---|---|---|
| Düğmesinden oluşturulan bir form üzerinde Panther submit | Evet | Evet | Evet |
| Form öğesinden oluşturulan bir form üzerinde Panther submit | Evet | Hayır | Hayır |
| executeScript ile çalıştırılan form.submit() | Hayır | Hayır | Hayır |
Doğrulamanın iki yüzü vardır. Boş bırakılmış zorunlu bir alan ya da bir e-posta alanının kabul etmeyeceği bir değer, PHP’de hiçbir hata vermeden tıklamayı durdurur ve hiçbir şey gönderilmez. Yukarıdaki bloğun yalnızca bir sonraki sayfada bulunan bir öğeyi beklemesinin nedenlerinden biri budur. Diğer neden ise Panther’ın crawler nesnesinin bir gönderimden sonra kısa bir süre daha eski sayfayı tarif edebilmesidir; bu yüzden getCrawler() sonucunu hemen okuyan kod formu yeniden görebilir.
5. Adım: callback’ten gönderim yapan sayfalar
Bazı sitelerde sıradan bir gönderim hiç yoktur. Widget bir data-callback özniteliği taşır ya da sayfa grecaptcha.render’a bir callback verir ve reCAPTCHA bu fonksiyona bir token verdiğinde fonksiyon formu gönderir. Invisible widget’lar neredeyse her zaman bu şekilde çalışır. Bu sayfalarda textarea’daki bir token, callback çalışana kadar hiçbir işe yaramaz; bu yüzden callback’i kendiniz çağırın. Önce textarea’yı doldurun, çünkü callback genellikle formu gönderir ve sunucu token’ı o alandan okur. Aşağıdaki script ikisini de yapar ve birden fazla widget bulunan bir sayfada bulduğu ilk callback’i çağırır:
// Writes the token into every reCAPTCHA textarea, then calls the widget's
// callback if it has one. ___grecaptcha_cfg is undocumented, so the walk
// looks for the object holding the sitekey instead of fixed key names.
const SUBMIT_TOKEN_JS = <<<'JS'
var token = arguments[0], found = null, seen = [];
document.querySelectorAll('textarea[name="g-recaptcha-response"]')
.forEach(function (t) { t.value = token; });
function walk(o, depth) {
if (found || !o || typeof o !== 'object' || depth > 6 || seen.indexOf(o) !== -1) return;
if (o instanceof Node || o === window) return;
seen.push(o);
if (Object.prototype.hasOwnProperty.call(o, 'sitekey') && o.callback) { found = o.callback; return; }
for (var k in o) {
if (Object.prototype.hasOwnProperty.call(o, k)) { try { walk(o[k], depth + 1); } catch (e) {} }
}
}
if (window.___grecaptcha_cfg) {
Object.keys(___grecaptcha_cfg.clients).forEach(function (id) { walk(___grecaptcha_cfg.clients[id], 0); });
}
if (typeof found === 'string') found = window[found];
if (typeof found !== 'function') return 'no callback';
found(token);
return 'callback called';
JS;
echo $client->executeScript(SUBMIT_TOKEN_JS, [$token]), PHP_EOL;reCAPTCHA her widget’ın ayarlarını ___grecaptcha_cfg adlı global bir nesnede tutar. Bir data-callback özniteliği orada fonksiyonun adı olarak, grecaptcha.render’a verilen bir callback ise fonksiyonun kendisi olarak yer alır; script ikisini de ele alır. Nesne belgelenmemiştir ve iç anahtar adları küçültülmüştür (minified); script’in sabit bir yolu izlemek yerine ayarları aramasının nedeni budur. Google bu yapıyı bir gün değiştirirse script hiçbir callback döndürmez; data-callback kullanan bir sayfada yine de özniteliği okuyup fonksiyonu o adla çağırabilirsiniz. Kaçınılması gereken tek hata, textarea’yı doldurmadan callback’i çağırmaktır: form boş bir g-recaptcha-response ile gider.
Tam çalışan örnek
İşte Symfony Panther CAPTCHA akışının tamamı tek bir betikte. Anahtarı her iki sayfa türünden de okur, çözer, token’ı yazar ve callback varsa gönderimi callback’e bırakır, yoksa düğmeye tıklar.
<?php
// composer require symfony/panther capskip/capskip
// signup.php: run from the project folder with php signup.php
require __DIR__ . '/vendor/autoload.php';
use CapSkip\CapSkip;
use CapSkip\Exceptions\CapSkipError;
use CapSkip\Exceptions\ValidationException;
use Symfony\Component\Panther\Client;
const PAGE_URL = 'https://example.com/signup';
// Writes the token into every reCAPTCHA textarea, then calls the widget's
// callback if it has one. ___grecaptcha_cfg is undocumented, so the walk
// looks for the object holding the sitekey instead of fixed key names.
const SUBMIT_TOKEN_JS = <<<'JS'
var token = arguments[0], found = null, seen = [];
document.querySelectorAll('textarea[name="g-recaptcha-response"]')
.forEach(function (t) { t.value = token; });
function walk(o, depth) {
if (found || !o || typeof o !== 'object' || depth > 6 || seen.indexOf(o) !== -1) return;
if (o instanceof Node || o === window) return;
seen.push(o);
if (Object.prototype.hasOwnProperty.call(o, 'sitekey') && o.callback) { found = o.callback; return; }
for (var k in o) {
if (Object.prototype.hasOwnProperty.call(o, k)) { try { walk(o[k], depth + 1); } catch (e) {} }
}
}
if (window.___grecaptcha_cfg) {
Object.keys(___grecaptcha_cfg.clients).forEach(function (id) { walk(___grecaptcha_cfg.clients[id], 0); });
}
if (typeof found === 'string') found = window[found];
if (typeof found !== 'function') return 'no callback';
found(token);
return 'callback called';
JS;
$solver = new CapSkip([
'apiKey' => getenv('CAPSKIP_API_KEY') ?: 'capskip',
'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
]);
// The full path: on Windows, Panther finds drivers\chromedriver.exe on
// its own but then fails to start it from the relative path.
$client = Client::createChromeClient(__DIR__ . '/drivers/chromedriver.exe');
try {
$client->request('GET', PAGE_URL);
// api.js adds the hidden textarea once the widget has rendered.
$client->waitFor('#g-recaptcha-response', 20);
$crawler = $client->getCrawler();
$box = $crawler->filter('[data-sitekey]');
if ($box->count() > 0) {
$sitekey = $box->attr('data-sitekey');
} else {
// grecaptcha.render() pages: the key is the k= parameter of the widget iframe.
$src = $crawler->filter('iframe[title="reCAPTCHA"]')->attr('src');
parse_str((string) parse_url($src, PHP_URL_QUERY), $query);
$sitekey = $query['k'] ?? '';
}
// Visible fields take typing as usual.
$form = $crawler->selectButton('Sign up')->form();
$form['email'] = 'YOUR_EMAIL';
$token = $solver->recaptcha($sitekey, $client->getCurrentURL())['code'];
if ($client->executeScript(SUBMIT_TOKEN_JS, [$token]) !== 'callback called') {
// No callback: click the real button, as a visitor would.
$client->submit($form);
}
// Wait for something only the next page has; getCrawler() can lag.
$client->waitFor('#welcome', 15);
echo 'Submitted: ', $client->getTitle(), PHP_EOL;
} catch (ValidationException $e) {
echo 'Not sent: ', $e->getMessage(), PHP_EOL;
} catch (CapSkipError $e) {
echo 'Solve failed: ', get_class($e), ': ', $e->getMessage(), PHP_EOL;
} finally {
$client->quit();
}Düğme etiketini, e-posta alanını ve beklediğiniz seçiciyi sayfanıza göre değiştirin. Invisible bir widget için 2. adımdaki seçenekler dizisini recaptcha çağrısına ekleyin. Hiç düğmesi olmayan bir callback sayfasında 1., 2. ve 5. adımlar işin tamamıdır. waitFor tıklamadan sonra pes ederse token’ı suçlamadan önce forma bakın: boş bırakılmış zorunlu bir alan, gönderimi hiçbir hata vermeden durdurur. Invisible ve Enterprise seçenekleri dahil SDK tarafının ayrıntılarını, recaptcha çağrısını tarayıcı olmadan ele alan PHP’de reCAPTCHA v2 çözme rehberinde bulabilirsiniz.
Panther ve CapSkip’i farklı makinelerde çalıştırmak
Panther test paketleri çoğu zaman bir CI runner’da ya da bir Linux makinesinde çalışır, CapSkip ise bir Windows uygulamasıdır. Bu düzen sorunsuz çalışır. CapSkip’i Server moduna alın; o zaman ağ adresinizi veya genel IP’nizi dinler, böylece herhangi bir makinedeki Panther onu aynı API üzerinden çağırabilir. Rota internetten geçiyorsa statik bir genel IP kullanın, API anahtarı doğrulamasını açın ve bir Windows Firewall kuralıyla portu yalnızca beklediğiniz adreslerle sınırlayın. Çözücü hâlâ sizin kendi Windows makinenizdir ve çözüm başına ücretlendirme hâlâ yoktur.
PHP SDK’sı ortam değişkenlerini kendiliğinden okumaz. Tam örnekte olduğu gibi CAPSKIP_HOST, CAPSKIP_PORT ve CAPSKIP_API_KEY değerlerini kodunuzda okuyup constructor’a verin; böylece aynı betik hem masanızda hem de CI’da çalışır. Host yanlışsa ya da CapSkip çalışmıyorsa ilk çağrı, bağlanamadığını söyleyen bir NetworkException ile başarısız olur. Portu dinleyen hiçbir şey yoksa bu yaklaşık iki saniye, bir güvenlik duvarı bağlantıyı düşürüyorsa SDK’nın bağlantı zaman aşımı olan 30 saniyeye kadar sürer.
Sık görülen hatalar ve anlamları
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| ElementNotInteractableException: element not interactable | Panther, token’ı gizli g-recaptcha-response textarea’sına yazmaya çalıştı | Değeri 3. adımdaki gibi executeScript ile yazın |
| Could not start chrome hatası ve bir komutun tanınmadığını söyleyen bir cmd.exe mesajı | Windows: Panther, chromedriver’ı drivers klasöründe buldu ama onu göreli yoldan başlatamadı | Driver’ın tam yolunu createChromeClient’a verin ya da drivers klasörünü PATH’e ekleyin |
| Could not start chrome hatası ve sistemin yolu bulamadığını söyleyen bir mesaj | createChromeClient’a verilen yolda chromedriver.exe yok | Proje klasöründe bdi detect drivers komutunu çalıştırın ya da yolu düzeltin |
| SessionNotCreatedException: This version of ChromeDriver only supports Chrome version N | Chrome kendini güncelledi, driver ise güncellenmedi | bdi detect drivers komutunu yeniden çalıştırın |
| RuntimeException: The port 9515 is already in use. | Başka bir Panther istemcisi ya da betiği chromedriver’ın varsayılan portunu zaten kullanıyor | createChromeClient’ın seçenekler dizisinde her istemciye kendi portunu verin |
| Token yazıldı ama hiçbir şey olmuyor | Sayfa gönderimi bir reCAPTCHA callback’inden yapıyor | Alanı yazan ve callback’i çağıran 5. adımdaki script’i çalıştırın |
| waitFor tıklamadan sonra zaman aşımına uğruyor ve hiçbir şey gönderilmedi | Zorunlu bir alan boştu ya da bir değer alanın doğrulamasından geçemedi | Göndermeden önce her zorunlu alanı geçerli bir değerle doldurun |
| NetworkException: Failed to connect to 127.0.0.1 port 8080 | CapSkip çalışmıyor ya da host ve port yanlış | CapSkip’i başlatın, ardından Local modunda mı yoksa Server modunda mı olması gerektiğini kontrol edin |
| Bir çözüm sırasında Maximum execution time exceeded hatası | Panther bir web isteğinin içinde çalışıyor ve Windows’ta bu sınır beklemede geçen süreyi de sayıyor | Panther’ı sınırın kapalı olduğu komut satırından ya da bir kuyruk worker’ından çalıştırın |
FAQ
Headless Chrome, CAPTCHA’yı geçmeyi zorlaştırır mı?
Bu yaklaşımda hayır. Challenge, Panther’ın yönettiği Chrome’da değil, CapSkip’in kendi tarayıcısında çözülür; bu yüzden Panther’ın tarayıcısının onu hiçbir zaman geçmesi gerekmez. Panther yalnızca hazır token’ı forma taşır. Çalıştırmayı izlemek için PHP’yi başlatmadan önce ortamda PANTHER_NO_HEADLESS değerini 1 yapın. Panther bu değeri $_SERVER üzerinden okur, bu yüzden betiğin içindeki bir putenv() çağrısının hiçbir etkisi yoktur.
Kendi uygulamamın Panther testleri bir çözücü kullanmalı mı?
Genellikle hayır. Kontrol ettiğiniz bir uygulama için test ortamını Google’ın yayımladığı reCAPTCHA test anahtarları ile yapılandırın. Bu anahtarlar doğrulamadan her zaman geçer, dolayısıyla testlerin hiçbir çözüme ihtiyacı olmaz. Bir çözücü, Panther’ın kontrolünüzde olmayan sayfaları ya da gerçek anahtarlarını korumak zorunda olan bir staging sitesini yönettiği durumlarda yerini hak eder.
Aynı anda birkaç Panther tarayıcısı çalıştırabilir miyim?
Evet; her istemci kendi chromedriver portunu aldığı sürece, süreç başına bir tane ya da tek bir süreçte birkaç tane. createChromeClient seçeneklerinde farklı bir port vermediğiniz sürece her istemci kendi chromedriver’ını 9515 portunda başlatır ve aynı porttaki ikinci istemci ilk isteğinde başarısız olur. PHP SDK’sı senkron olduğu için her süreç aynı anda bir CAPTCHA çözer; CapSkip’in kendisi ise varsayılan olarak aynı anda 10 reCAPTCHA çözümü çalıştırır.
Aynı yaklaşım Cloudflare Turnstile için de işe yarar mı?
Yapı aynıdır. Turnstile kendi token’ını cf-turnstile-response adlı gizli bir input alanına koyar. Panther onu da dolduramaz: Form nesnesi geçersiz bir öğe durumu (invalid element state) bildirir, sendKeys öğenin etkileşime açık olmadığını bildirir, executeScript ise değeri ayarlar. Onu recaptcha yerine SDK’nın turnstile metoduyla çözün ve sitekey’i cf-turnstile div’inin data-sitekey özniteliğinden okuyun. 5. adımdaki script yalnızca reCAPTCHA callback’lerini ele alır.
Kısa özet
Symfony Panther CAPTCHA çözümü için Windows’ta driver’ın tam yolunu createChromeClient’a verin, sayfayı yükleyin ve g-recaptcha-response textarea’sını bekleyin, ardından sitekey’i data-sitekey özniteliğinden ya da widget iframe’inden okuyun. Panther’ın geçerli URL’sini kullanarak PHP SDK ile çözün. Token’ı asla Form nesnesiyle değil, executeScript ile yazın, ardından gerçek düğmeye tıklayın ya da callback’i olan sayfalarda callback’i çağırın. Bir sonraki sayfayı okumadan önce onu bekleyin ve Panther başka bir yerde çalışıyorsa CapSkip’i Server moduna alın.
- PHP paketinin desteklediği tüm CAPTCHA türleri: PHP CAPTCHA çözücü sayfası.
- Onay kutulu ve invisible reCAPTCHA v2’nin nasıl çözüldüğü: reCAPTCHA v2 çözücü sayfası.
Her sayfada bir widget ile karşılaşan bir Panther kazıyıcısı, captcha atlatma aracı kendi Windows makinenizde çalıştığında fazladan hiçbir şey ödemez. Üzerinde bir challenge var diye bir sayfayı atlamanız için hiçbir neden yok.
