Python Kazıyıcılarda CAPTCHA ile Engel Nasıl Ayırt Edilir

Bir Python scraper’ı kötü bir yanıt aldığında ilk soru CAPTCHA mı engel mi olduğudur, çünkü cevap bundan sonra ne yapılacağını belirler. Bir CAPTCHA çözülebilir. Bir hız sınırı bekleme gerektirir. Bir engel sayfası, adresinizin ya da parmak izinizin reddedildiği anlamına gelir ve hiçbir token bunu değiştirmez. Bu rehber, durumu, başlıkları ve işaretlemeyi okuyup altı etiketten birini döndüren yaklaşık 40 satırlık bir sınıflandırıcı ve ardından CapSkip’i yalnızca çözümün işe yarayabileceği etiket için çağıran bir fetch döngüsü oluşturur. CapSkip kendi Windows bilgisayarınızda çalışır ve çözüm başına ücret yoktur; bu yüzden işe yaramayacak bir çözümü atlamak para değil, zaman kazandırır.
Neye ihtiyacınız var
- reCAPTCHA çözücüsü açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatır.
- Python 3.10 veya üzeri. Aşağıdaki kod Python 3.14.0 üzerinde, requests 2.33.0 ve capskip 1.3.0 ile çalıştırıldı.
- Bir PowerShell istemi. Komutlar PowerShell sözdizimini kullanır.
- Sahibi olduğunuz ya da otomatikleştirmenize izin verilen sayfalar.
- Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ağ adresinizi veya genel IP’nizi dinler, böylece başka bir bilgisayardaki, bir VPS’teki veya barındırılan bir platformdaki scraper API üzerinden ona erişebilir. İkisi de şurada ayarlanır: bağlantı ayarları.
Nasıl test edildi: sınıflandırıcı ve fetch döngüsü Python 3.14.0 üzerinde 17 denetimi geçti; her biri yerel bir fixture sitesinin sunduğu bir yanıttı. Çözümler, uygulamaya değil bir mock CapSkip’e, gerçek capskip paketi üzerinden gitti. Cloudflare’in tam sayfa denetiminin arkasındaki iki herkese açık sayfa da 29 Ekim 2026’da çekildi ve ikisi de cf-mitigated başlığıyla 403 döndürdü. Aşağıdaki hiçbir sayı bir çözüm süresi değildir.
CAPTCHA mı engel mi neden ilk sorudur
İki hata farklı şeylere mal olur. Bir engel sayfasını çözücüye göndermek çözümü boşa harcar ve sizi engelli bırakır. Bir CAPTCHA çıktığında her seferinde yeni bir proxy’ye geçmek, tüketmeniz gerekmeyen adresleri tüketir. CAPTCHA sunan bir sayfa genellikle sizden bir şey kanıtlamanızı ister ve çözülmüş bir token sizi içeri alır. Engel sunan bir sayfa sizi tamamen reddediyordur. HTTP 429 kılavuzu bunun bekleme tarafını tek bir durum kodu için ele alır. Bu rehber ondan önce gelen kararı ele alır.
Altı etiket her şeyi kapsar: ok, rate_limited, captcha, cloudflare_challenge, blocked ve unknown. unknown etiketi önemlidir. Geri kalan beşinden birini seçmek zorunda olan bir sınıflandırıcı bazen yanlış seçer ve unknown üzerinde hata fırlatan bir döngü size henüz ele almadığınız durumu gösterir.
1. Adım: kurulum
Boş bir klasörde sanal ortam oluşturun ve iki paketi kurun. Komutlar ortamın kendi Python’unu yolla çağırır, bu yüzden onu etkinleştirmeniz gerekmez.
py -m venv .venv .\.venv\Scripts\python.exe -m pip install capskip requests
2. Adım: sinyaller
Bu bloğu ve sonrakini classify.py olarak kaydedin. İlk blok desenleri içerir. Widget deseni, class özniteliği bir widget adını içeren bir etiketi ya da bir altcha-widget etiketini arar. İşaretlemeyi kaçışlı kodun içinde gösteren bir blog yazısı, adresi bir widget adı içeren bir bağlantı ve gizli bir g-recaptcha-response girdisi eşleşmez.
Son satır, bir yumuşak engeli, yani normal bir durum döndüren ama reddedildiğinizi söyleyen bir sayfayı tanımlayan ifadeleri içerir. Dikkatli olun. Cloudflare’in hata belgeleri, ilk iki ifadenin geldiği Error 1020: Access denied maddesini listeler. Diğer ikisi, sahada görülen engel sayfalarından yapılmış tahminlerdir ve hedefinizin kendi ifadesi olacaktır. Gerçek birkaçını gördükten sonra onları değiştirin.
import re
# A tag whose class attribute names a widget, or an altcha-widget tag. A link, a hidden
# input or a sentence that merely contains the name does not match.
CLS = r"<[^>]*\bclass\s*=\s*[\"'][^\"']*(?<![\w-])%s(?![\w-])"
WIDGET = re.compile(CLS % "(?:g-recaptcha|h-captcha|cf-turnstile|frc-captcha|captchafox)"
+ r"|<altcha-widget\b", re.I)
TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
BLOCK = ("access denied", "error 1020", "unusual traffic", "you have been blocked")3. Adım: CAPTCHA mı engel mi karar verin
Denetimlerin sırası mantığın kendisidir. Her kural yalnızca üstündekilerden hiçbiri eşleşmediyse çalışır.
- cf-mitigated başlığı ilk gelir. Cloudflare, challenge değerinin challenge sayfalarında gönderdiği değer olduğunu belgeler ve bu, fonksiyondaki en güçlü sinyaldir.
- Yönlendirme izlemeyi kapatan çağıranlar için bir yönlendirme sonra denetlenir. Buna unknown denir, çünkü adres tek başına bunun bir CAPTCHA’ya mı yoksa adında bu sözcük geçen bir makaleye mi gittiğini söyleyemez.
- _cf_chl_opt script değişkeni ve Just a moment başlığı bundan sonra gelir. Bir proxy ya da CDN başlığı ayıklamışsa challenge sayfasını yakalarlar.
- expect argümanı, yalnızca gerçek sayfanın içerdiği bir dizeyi adlandırmanızı sağlar. Mevcut olduğunda sayfa, durum ne olursa olsun ok sayılır. O olmadan, alt bilgisinde bir CAPTCHA widget’ı olan bir içerik sayfası CAPTCHA sayılırdı.
- İşaretlemedeki bir widget, 429 ya da 403 üzerinde bile captcha demektir. Bu, durum kurallarından önce çalışır, çünkü bazı siteler CAPTCHA’larını bir hata durumuyla sunar. Widget listesi reCAPTCHA, hCaptcha, Turnstile, Friendly Captcha, CaptchaFox ve ALTCHA’yı kapsar.
- Bir 429 ya da Retry-After içeren bir 503 rate_limited demektir. HTML gövdeli bir 403, sayfa metni csrf içermedikçe blocked demektir. JSON bir 403 unknown olarak bırakılır, çünkü token’ınızda bir kapsam eksik diyen bir API size proxy değiştirmenizi söylemiyordur.
- Durumu 400’ün altında olan bir yanıt, başlığı ifadelerden birini içerdiğinde ya da sayfanın tamamı 3.000 karakterden kısa olup birini içerdiğinde bir yumuşak engeldir.
def classify(resp, expect=None):
"""expect: a string only the real page contains, for pages that carry a widget anyway."""
code, h = resp.status_code, resp.headers
if h.get("cf-mitigated", "").lower() == "challenge":
return "cloudflare_challenge" # Cloudflare docs
if 300 <= code < 400: # allow_redirects=False callers
return "unknown"
html = "html" in h.get("content-type", "").lower()
text = resp.text[:200000] if html else ""
low = text.lower()
if "_cf_chl_opt" in text or "<title>just a moment" in low:
return "cloudflare_challenge" # header stripped by a proxy
if expect and expect in text:
return "ok"
if WIDGET.search(text):
return "captcha" # even on a 429 or a 403
if code == 429 or (code == 503 and "retry-after" in h):
return "rate_limited"
if code == 403 and html and "csrf" not in low:
return "blocked" # JSON 403s fall through
m = TITLE.search(text)
head = m.group(1).lower() if m else ""
small = len(text) < 3000 # block pages are short
if code < 400 and html and any(b in head or (small and b in low) for b in BLOCK):
return "blocked" # soft block: 200 with a notice
return "ok" if code < 400 else "unknown"4. Adım: Retry-After değerini okuyun
Bu bloğu, sonrakini ve ondan sonrakini policy.py olarak kaydedin. Bir 429 bir Retry-After başlığı taşıyabilir ve başlık saniye ya da bir HTTP tarihi içerebilir. MDN ikisini de listeler ve başlığın 429’da isteğe bağlı olduğunu söyler. Yardımcı ikisini de ele alır ve başlık eksik ya da okunamazsa sizin seçtiğiniz bir sayıya döner.
import re
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import urljoin
from classify import classify
def retry_delay(resp, fallback):
raw = resp.headers.get("Retry-After")
if not raw:
return fallback
try:
return max(1.0, float(raw)) # delay-seconds
except ValueError:
pass
try:
when = parsedate_to_datetime(raw) # HTTP-date
return max(1.0, (when - datetime.now(timezone.utc)).total_seconds())
except (TypeError, ValueError):
return fallback5. Adım: CAPTCHA’yı çözün
CapSkip’in çağrıldığı tek yer burasıdır. Sitekey’i ve form action değerini sayfadan okur, CapSkip’ten bir token ister ve onu g-recaptcha-response alanında geri gönderir. Bilerek yalnızca reCAPTCHA v2’yi ele alır ve yalnızca o tek alanı gönderir, başka bir şey göndermez; bu yüzden başka zorunlu alanları olan bir forma bunların data’ya eklenmesi gerekir. Listedeki her widget captcha etiketini alır, bu yüzden bir Turnstile, hCaptcha, Friendly Captcha, CaptchaFox ya da ALTCHA sayfası da bu fonksiyona ulaşır. Onları, önce CAPTCHA türü algılayıcısını çalıştırmanızı söyleyen bir RuntimeError ile durdurur. Bunun yerine bir Turnstile anahtarını reCAPTCHA metoduna göndermek ERROR_GOOGLEKEY ile sonuçlanırdı: CapSkip, reCAPTCHA anahtarına benzemeyen bir anahtarı reddeder; bunu uygulamada değil, uygulamanın anahtar denetimini kopyalayan bir mock üzerinde doğruladım. reCAPTCHA çağrısının kendisi için şuraya bakın: reCAPTCHA v2 çözücü sayfası.
def solve_recaptcha_form(session, resp, solver):
"""reCAPTCHA v2 only. For other types, run the detector first."""
key = re.search(r"data-sitekey=[\"']([^\"']+)", resp.text)
form = re.search(r"<form[^>]*action=[\"']([^\"']*)", resp.text)
if "g-recaptcha" not in resp.text or not (key and form):
raise RuntimeError("not a plain reCAPTCHA v2 form: run the detector first")
token = solver.recaptcha(sitekey=key.group(1), url=resp.url)["code"]
return session.post(urljoin(resp.url, form.group(1)),
data={"g-recaptcha-response": token}, timeout=30)6. Adım: fetch döngüsü
Döngü, CAPTCHA mı engel mi kararına göre davranır. Her etiketi tek bir eyleme çevirir ve en fazla dört kez dener. Bir hız sınırı, başlık değeri kadar bekler; alt sınır 1 saniye, üst sınır 60. Üst sınır, bir günlük bir Retry-After değerinin bir dakika sonra yeniden denendiği anlamına gelir; bu da sınırı uzatabilir. Retry-After içermeyen çıplak bir 429, 5 saniyelik iki bekleme alır ve ardından proxy değişimi yapılır; yeni adres için sayım yeniden başlar. Dört denemeyle bu iki bekleme, bir proxy değişimi ve bir bekleme daha demektir. Retry-After taşıyan bir 429 asla proxy değiştirmez. Bir engel rotate fonksiyonunuzu çağırır ve çözücüye asla ulaşmaz. Rotate fonksiyonunuz bir sonraki isteğin görünümünü değiştirmeli, örneğin session.proxies ayarlamalı ya da çalışmayı durdurmak için hata fırlatmalıdır. Bir challenge sayfası on_challenge fonksiyonunuzu çağırır; bu fonksiyon session.cookies üzerinde geçerli bir cf_clearance çerezi bırakmalıdır, çünkü Turnstile metodunun tam sayfa challenge için ihtiyaç duyduğu değerler düz bir isteğin aldığı HTML’de yoktur. İki fonksiyondan biri döndükten sonra döngü URL’yi basitçe yeniden ister. Başarıda fonksiyon form gönderiminin yanıtını döndürür, bu yüzden hedef sayfanız farklıysa onu yeniden isteyin. Her CAPTCHA denemesi bir çözüme mal olur ve beş dakikaya kadar bekleyebilir, bu yüzden başarısız dört deneme dört çözüm harcayabilir. Bir NetworkException, CapSkip’e ulaşılamadığı anlamına gelir ve burada yakalanmaz. cf_clearance kılavuzu ardından gelen çerezi ele alır.
def fetch(session, url, solver, rotate, on_challenge,
tries=4, sleep=time.sleep, expect=None):
limited = 0
for _ in range(tries):
resp = session.get(url, timeout=30)
kind = classify(resp, expect)
if kind == "ok":
return resp
if kind == "rate_limited":
limited += 1
if limited > 2 and "Retry-After" not in resp.headers:
rotate(session) # a bare 429 that outlasts two waits
limited = 0 # the new address starts fresh
else:
sleep(min(retry_delay(resp, 5.0), 60.0))
elif kind == "captcha":
resp = solve_recaptcha_form(session, resp, solver)
if classify(resp, expect) == "ok":
return resp
elif kind == "cloudflare_challenge":
on_challenge(session, resp) # yours: a browser, or the Turnstile guide
elif kind == "blocked":
rotate(session) # never solve a block
else:
raise RuntimeError(f"unknown response {resp.status_code} from {url}")
raise RuntimeError(f"gave up on {url} after {tries} tries")Çalıştırın
Bunu diğer iki dosyanın yanına use.py olarak kaydedin. Adresi değiştirin ve kendi proxy mantığınızı rotate içine koyun. Örnekteki rotate hata fırlatır, bu yüzden engellenmiş bir çalışma aynı adresi dört kez istemek yerine durur. Ortamın kendi Python’uyla, .\.venv\Scripts\python.exe use.py örneğindeki gibi çalıştırın. 8080 portu varsayılandır; sizinki farklıysa CapSkip’in bağlantı ayarlarında gösterilen portu kullanın. Yer tutucu adresle script, beklenen sonuç olan bir unknown response hatasıyla biter.
import requests
from capskip import CapSkip
from policy import fetch
solver = CapSkip(host="127.0.0.1", port=8080)
def rotate(session):
raise RuntimeError("blocked: change the proxy here, or stop the run")
def on_challenge(session, resp):
raise RuntimeError("Cloudflare challenge page: needs a browser")
session = requests.Session()
page = fetch(session, "https://example.com/data", solver, rotate, on_challenge)
print(page.status_code, len(page.text))Sınıflandırıcıya güvenmeden önce gerçek yanıtları nasıl değerlendirdiğini görmek için bu denetimi kendi birkaç URL’nizde çalıştırın. 29 Ekim 2026’daki bir çalıştırmada düz bir sayfa için ok, bir 429 için rate_limited ve bir test servisinden gelen 403 için blocked yazdırdı.
import requests
from classify import classify
URLS = [
"https://example.com/",
"https://httpbin.org/status/429",
"https://httpbin.org/status/403",
]
for url in URLS:
resp = requests.get(url, timeout=30)
print(classify(resp), resp.status_code, url)Sınıflandırıcının yanıldığı yerler
Bu bir sezgisel kurallar kümesidir ve şu durumlar bilinmektedir.
| Durum | Sonuç | Düzeltme |
|---|---|---|
| Alt bilgisinde CAPTCHA widget’ı olan içerik sayfası | captcha | classify veya fetch’e, yalnızca gerçek sayfanın içerdiği bir dizeyle expect geçirin |
| Başlığında unusual traffic ifadesi geçen blog yazısı | blocked | BLOCK içindeki ifadeleri hedefinizin kendi ifadeleriyle değiştirin |
| Uygulamanın kendisinden gelen HTML 403, örneğin bir giriş sayfası | blocked | 403 kuralından önce o sayfa için bir kural ekleyin |
| Yalnızca görsel içeren bir /captcha sayfasına doğrudan ziyaret ya da yönlendirme | ok | O site için bir kural ekleyin |
| g-recaptcha sınıfına ve data-action özniteliğine sahip düğme (reCAPTCHA v3 bağlaması) | captcha | Bu widget sayfaya girişi hiçbir zaman engellemez: expect geçirin ya da algılayıcıyı çalıştırın |
| Yüklemeden sonra JavaScript ile eklenen CAPTCHA | ok | Sınıflandırıcı yalnızca ilk yanıtı görür |
| Site genelinde reCAPTCHA v3 script’i | ok | Amaçlanan davranış: v3 sayfaya girişi hiçbir zaman engellemez |
Yaygın hatalar
| Hata | Neden | Düzeltme |
|---|---|---|
| RuntimeError: unknown response | Kuralların kapsamadığı bir durum, örneğin JSON bir 403 ya da bir 404 | Gövdeyi yazdırın ve bir kural ekleyin ya da çalışmayı durdurmasına izin verin |
| RuntimeError: gave up | ok olmadan dört deneme | Hangi etiketin tekrarlandığını kontrol edin ve tries değerini yalnızca rate_limited ise artırın |
| RuntimeError: not a plain reCAPTCHA v2 form | captcha etiketi başka bir widget’tan geldi ya da sayfada sitekey veya form yok | Algılayıcıyı çalıştırın ve eşleşen metodu çağırın |
| ERROR_GOOGLEKEY | Anahtar bir reCAPTCHA anahtarına benzemiyor, örneğin bir Turnstile anahtarı ya da explicit kelimesi | Anahtarı doğrudan widget’tan okuyun ya da algılayıcıyı çalıştırın |
| ApiException veya TimeoutException | Çözüm başarısız oldu ya da SDK’nın sınırını aştı | Öncesindeki etiketi kontrol edin ve başarısız bir CAPTCHA çok sayıda çözüm harcamasın diye tries değerini düşük tutun |
| NetworkException | CapSkip çalışmıyor ya da host veya port yanlış | CapSkip’i başlatın ve bağlantı ayarlarını kontrol edin, şuraya bakın: API belgeleri |
Çözücüyü bir sunucuda çalıştırmak
Döngünün CapSkip için yalnızca bir host ve bir porta ihtiyacı vardır. Local modunda bu, aynı bilgisayardaki 127.0.0.1’dir. Server modunda CapSkip ağ adresinizi ya da genel bir IP’yi dinler, böylece başka bir bilgisayardaki, bir VPS’teki ya da barındırılan bir platformdaki scraper aynı API’yi çağırır. CapSkip constructor’ında host ve portu değiştirin, başka hiçbir şey değişmez. Kararlı uzaktan erişim için statik bir genel IP önerilir. Server modu yine kendi donanımınızdır ve yine kullanım başına ücretlendirilmez. Portu her adrese açar, bu yüzden ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Modları nasıl değiştireceğinizi bağlantı ayarları bölümü anlatır.
FAQ
Engellendiğimi mi yoksa yalnızca hız sınırına mı takıldığımı nasıl anlarım?
Özellikle Retry-After başlığı olan bir 429 bir hız sınırıdır ve beklemek işe yarar. HTML gövdeli bir 403 ya da erişimin reddedildiğini söyleyen bir 200 sayfası bir engeldir. İki beklemeden uzun süren çıplak bir 429, pratik bir kural olarak engel sayılır; döngünün onda proxy değiştirmesinin nedeni budur.
Çözülmüş bir CAPTCHA bir engeli giderebilir mi?
Hayır. Bir engel adresinizi ya da parmak izinizi reddeder ve bir token bunlardan hiçbiri hakkında bir şey kanıtlamaz. Döngü bir engelde çözücüyü atlar ve onun yerine proxy değiştirir.
Döngü neden Cloudflare challenge sayfasını çözmüyor?
Turnstile metodu challenge’dan iki değer ister ve düz bir istek bunları almaz. Döngü bu durumu sizin sağladığınız bir fonksiyona, örneğin gerçek bir tarayıcıyı süren birine, devreder ve sınıflandırıcıyı neyi görebildiği konusunda dürüst tutar.
Bu, başka bir makinedeki CapSkip’e karşı çalışabilir mi?
Evet. Server modunu kullanın, CapSkip constructor’ını o host ve porta yöneltin; döngü tek bir bilgisayardaki gibi çalışır. Çözücü sizin donanımınızda kalır.
Kısa özet
Önce CAPTCHA mı engel mi karar verin, sonra harekete geçin: CAPTCHA’yı çözün, hız sınırını bekleyin, engelden proxy değiştirerek uzaklaşın ve bilinmeyen her şeyde durun. CapSkip kendi makinenizde olduğunda, boşa gidecek bir çözümü atlamaktan gelen kazanç zamandır, gereksiz yere proxy değiştirmemekten gelen kazanç ise adreslerdir. CAPTCHA sayfalarını aşmaya dair daha geniş bir bakış için captcha atlatma yaklaşımı, çözülebilenleri çözmek ve geri kalanı dolanmaktır. Python SDK’nın kendisi için şuraya bakın: Python CAPTCHA çözücü sayfası.
