Cara Membedakan CAPTCHA atau Blokir pada Scraper Python

Ketika scraper Python mendapat respons yang buruk, pertanyaan pertama adalah CAPTCHA atau blokir, karena jawabannya menentukan langkah berikutnya. CAPTCHA bisa dipecahkan. Rate limit perlu ditunggu. Halaman blokir berarti alamat atau fingerprint Anda ditolak, dan token apa pun tidak akan mengubahnya. Panduan ini membangun classifier sepanjang sekitar 40 baris yang membaca status, header, dan markup lalu mengembalikan salah satu dari enam label, kemudian fetch loop yang memanggil CapSkip hanya untuk label yang pemecahannya bisa membantu. CapSkip berjalan di PC Windows Anda sendiri tanpa biaya per pemecahan, jadi melewati pemecahan yang tidak mungkin berhasil menghemat waktu, bukan uang.
Apa yang Anda butuhkan
- CapSkip yang berjalan di PC Windows, dengan pemecah reCAPTCHA diaktifkan. Lihat panduan penyiapan untuk menjalankannya pertama kali.
- Python 3.10 atau lebih baru. Kode di bawah dijalankan pada Python 3.14.0 dengan requests 2.33.0 dan capskip 1.3.0.
- Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
- Halaman milik Anda atau yang boleh Anda otomatisasi.
- Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga scraper di PC lain, VPS, atau platform hosting dapat menjangkaunya melalui API. Keduanya diatur di pengaturan koneksi.
Cara pengujiannya: classifier dan fetch loop lulus 17 pemeriksaan pada Python 3.14.0, masing-masing berupa respons yang disajikan oleh situs fixture lokal. Pemecahan dijalankan melalui paket capskip yang asli ke mock CapSkip, bukan ke aplikasinya. Dua halaman publik di balik pemeriksaan halaman penuh Cloudflare juga diambil pada 29 Oktober 2026, dan keduanya mengembalikan 403 dengan header cf-mitigated. Tidak ada angka di bawah ini yang merupakan waktu pemecahan.
Mengapa CAPTCHA atau blokir adalah pertanyaan pertama
Kedua kesalahan itu menimbulkan biaya yang berbeda. Mengirim halaman blokir ke pemecah menyia-nyiakan satu pemecahan dan Anda tetap terblokir. Berganti ke proxy baru setiap kali CAPTCHA muncul menghabiskan alamat yang sebenarnya tidak perlu dihabiskan. Halaman yang menyajikan CAPTCHA biasanya meminta Anda membuktikan sesuatu, dan token hasil pemecahan membuat Anda lolos. Halaman yang menyajikan blokir menolak Anda secara langsung. Panduan HTTP 429 membahas sisi menunggu dari hal ini untuk satu kode status. Panduan ini membahas keputusan yang datang sebelumnya.
Enam label mencakup semuanya: ok, rate_limited, captcha, cloudflare_challenge, blocked, dan unknown. Label unknown itu penting. Classifier yang terpaksa memilih di antara lima lainnya kadang akan salah memilih, dan loop yang melempar error pada unknown menunjukkan kasus yang belum Anda tangani.
Langkah 1: instal
Buat virtual environment di folder kosong lalu instal dua paket. Perintahnya memanggil Python bawaan environment itu lewat path, jadi Anda tidak perlu mengaktifkannya.
py -m venv .venv .\.venv\Scripts\python.exe -m pip install capskip requests
Langkah 2: sinyal
Simpan blok ini dan blok berikutnya sebagai classify.py. Blok pertama berisi pola. Pola widget mencari tag yang atribut class-nya menyebut sebuah widget, atau tag altcha-widget. Tulisan blog yang menampilkan markup di dalam kode yang di-escape, tautan yang alamatnya memuat nama widget, dan input g-recaptcha-response tersembunyi semuanya gagal cocok.
Baris terakhir berisi frasa yang mengidentifikasi soft block, yaitu halaman yang mengembalikan status normal tetapi menyatakan Anda ditolak. Berhati-hatilah dengannya. Dokumentasi error Cloudflare mencantumkan Error 1020: Access denied, dari situlah dua frasa pertama berasal. Dua lainnya adalah tebakan dari halaman blokir di dunia nyata, dan target Anda akan punya kata-katanya sendiri. Gantilah setelah Anda melihat beberapa yang sungguhan.
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")Langkah 3: putuskan CAPTCHA atau blokir
Urutan pemeriksaan adalah logikanya. Setiap aturan hanya dijalankan bila aturan di atasnya tidak cocok.
- Header cf-mitigated diperiksa lebih dulu. Cloudflare mendokumentasikan nilai challenge sebagai nilai yang dikirimkannya pada halaman challenge, dan itu sinyal terkuat dalam fungsi ini.
- Pengalihan diperiksa berikutnya, untuk pemanggil yang mematikan pengikutan redirect. Hasilnya disebut unknown, karena alamat saja tidak bisa menyatakan apakah ia mengarah ke CAPTCHA atau ke artikel yang namanya memuat kata itu.
- Variabel skrip _cf_chl_opt dan judul Just a moment diperiksa setelah itu. Keduanya menangkap halaman challenge ketika proxy atau CDN menghapus header-nya.
- Argumen expect memungkinkan Anda menyebut string yang hanya ada di halaman yang sebenarnya. Bila ada, halaman disebut ok, apa pun statusnya. Tanpanya, halaman konten dengan widget CAPTCHA di footer akan disebut CAPTCHA.
- Widget di markup berarti captcha, bahkan pada 429 atau 403. Ini dijalankan sebelum aturan status, karena beberapa situs menyajikan CAPTCHA dengan status error. Daftar widget mencakup reCAPTCHA, hCaptcha, Turnstile, Friendly Captcha, CaptchaFox, dan ALTCHA.
- Respons 429, atau 503 dengan Retry-After, berarti rate_limited. Respons 403 dengan body HTML berarti blocked, kecuali teks halaman memuat csrf. Respons 403 JSON dibiarkan sebagai unknown, karena API yang menyatakan token Anda kekurangan scope tidak menyuruh Anda berganti proxy.
- Respons dengan status di bawah 400 adalah soft block bila judulnya memuat salah satu frasa, atau bila seluruh halaman lebih pendek dari 3.000 karakter dan memuat salah satunya.
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"Langkah 4: baca Retry-After
Simpan blok ini, blok berikutnya, dan blok sesudahnya sebagai policy.py. Respons 429 mungkin membawa header Retry-After, dan header itu bisa berisi detik atau tanggal HTTP. MDN mencantumkan kedua bentuk itu, dan menyatakan bahwa header itu opsional pada 429. Fungsi bantu menangani keduanya dan kembali ke angka pilihan Anda bila header hilang atau tidak terbaca.
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 fallbackLangkah 5: pecahkan CAPTCHA
Ini satu-satunya tempat CapSkip dipanggil. Fungsi ini membaca sitekey dan action formulir dari halaman, meminta token dari CapSkip, dan mengirimkannya kembali di field g-recaptcha-response. Fungsi ini hanya menangani reCAPTCHA v2, dengan sengaja, dan hanya mengirim satu field itu tanpa yang lain, jadi formulir dengan field wajib lain memerlukan field itu ditambahkan ke data. Setiap widget dalam daftar mendapat label captcha, jadi halaman Turnstile, hCaptcha, Friendly Captcha, CaptchaFox, atau ALTCHA juga sampai ke fungsi ini. Halaman-halaman itu dihentikan dengan RuntimeError yang menyuruh Anda menjalankan detektor jenis CAPTCHA lebih dulu. Mengirim kunci Turnstile ke metode reCAPTCHA akan berakhir dengan ERROR_GOOGLEKEY: CapSkip menolak kunci yang tidak tampak seperti kunci reCAPTCHA, yang saya konfirmasi pada mock yang meniru pemeriksaan kunci aplikasi, bukan pada aplikasinya. Untuk panggilan reCAPTCHA itu sendiri, lihat Halaman pemecah reCAPTCHA v2.
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)Langkah 6: fetch loop
Loop ini bertindak berdasarkan keputusan CAPTCHA atau blokir. Loop mengubah setiap label menjadi satu tindakan dan mencoba hingga empat kali. Rate limit membuat loop menunggu selama nilai header, dengan batas bawah 1 detik dan batas atas 60. Batas atas itu berarti Retry-After sehari dicoba lagi setelah satu menit, yang bisa memperpanjang limit. Sebuah 429 polos, yang tanpa Retry-After, mendapat dua kali tunggu 5 detik lalu pergantian, dan hitungan dimulai lagi untuk alamat baru. Dengan empat percobaan, itu berarti dua kali tunggu, satu pergantian, dan satu kali tunggu lagi. Respons 429 yang membawa Retry-After tidak pernah berganti. Blokir memanggil fungsi rotate Anda dan tidak pernah sampai ke pemecah. Fungsi rotate Anda harus mengubah tampilan permintaan berikutnya, misalnya dengan mengatur session.proxies, atau memunculkan exception untuk menghentikan eksekusi. Halaman challenge memanggil fungsi on_challenge Anda, yang harus meninggalkan cookie cf_clearance yang valid di session.cookies, karena nilai yang dibutuhkan metode Turnstile untuk challenge halaman penuh tidak ada di HTML yang diterima permintaan biasa. Setelah salah satu fungsi selesai, loop cukup meminta URL itu lagi. Bila berhasil, fungsi mengembalikan respons dari post formulir, jadi bila halaman target Anda berbeda, minta lagi halaman itu. Setiap percobaan CAPTCHA memakai satu pemecahan dan bisa menunggu hingga lima menit, jadi empat percobaan yang gagal bisa memakai empat pemecahan. NetworkException berarti CapSkip tidak dapat dijangkau dan tidak ditangkap di sini. Panduan cf_clearance membahas cookie yang datang sesudahnya.
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")Jalankan
Simpan ini sebagai use.py di samping dua file lainnya. Ganti alamatnya, dan taruh logika proxy Anda sendiri di rotate. Contoh rotate memunculkan exception, jadi eksekusi yang diblokir berhenti alih-alih meminta alamat yang sama empat kali. Jalankan dengan Python bawaan environment, seperti pada .\.venv\Scripts\python.exe use.py. Port 8080 adalah default; pakai port yang ditampilkan di pengaturan koneksi CapSkip bila port Anda berbeda. Dengan alamat placeholder, skrip berakhir dengan error respons unknown, yang merupakan hasil yang diharapkan.
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))Untuk melihat bagaimana classifier menilai respons nyata sebelum Anda mempercayainya, jalankan pemeriksaan ini pada beberapa URL Anda sendiri. Pada eksekusi tanggal 29 Oktober 2026, classifier mencetak ok untuk halaman biasa, rate_limited untuk 429, dan blocked untuk 403 dari layanan uji.
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)Tempat classifier salah
Ini sekumpulan heuristik, dan kasus-kasus berikut sudah diketahui.
| Kasus | Hasil | Perbaiki |
|---|---|---|
| Halaman konten dengan widget CAPTCHA di footer | captcha | Berikan expect ke classify, atau ke fetch, dengan string yang hanya dimiliki halaman yang sebenarnya |
| Tulisan blog dengan unusual traffic di judulnya | blocked | Ganti frasa di BLOCK dengan frasa milik target Anda sendiri |
| Respons 403 HTML dari aplikasi itu sendiri, seperti halaman login | blocked | Tambahkan aturan untuk halaman itu sebelum aturan 403 |
| Kunjungan langsung, atau redirect, ke halaman /captcha yang hanya berisi gambar | ok | Tambahkan aturan untuk situs itu |
| Tombol dengan class g-recaptcha dan atribut data-action (binding reCAPTCHA v3) | captcha | Widget itu tidak pernah menjaga halaman: berikan expect, atau jalankan detektor |
| CAPTCHA ditambahkan oleh JavaScript setelah dimuat | ok | Classifier hanya melihat respons pertama |
| Skrip reCAPTCHA v3 di seluruh situs | ok | Disengaja: v3 tidak pernah menjaga halaman |
Kesalahan umum
| Kesalahan | Penyebab | Perbaiki |
|---|---|---|
| RuntimeError: unknown response | Status yang tidak dicakup aturan, seperti 403 JSON atau 404 | Cetak body dan tambahkan aturan, atau biarkan menghentikan eksekusi |
| RuntimeError: gave up | Empat percobaan tanpa ok | Periksa label mana yang berulang, dan naikkan tries hanya bila itu rate_limited |
| RuntimeError: not a plain reCAPTCHA v2 form | Label captcha berasal dari widget lain, atau halaman tidak punya sitekey atau formulir | Jalankan detektor dan panggil metode yang sesuai |
| ERROR_GOOGLEKEY | Kunci tidak tampak seperti kunci reCAPTCHA, misalnya kunci Turnstile atau kata explicit | Baca kunci dari widget itu sendiri, atau jalankan detektor |
| ApiException atau TimeoutException | Pemecahan gagal atau melewati batas SDK | Periksa label yang mendahuluinya, dan jaga tries tetap rendah agar CAPTCHA yang gagal tidak memakai banyak pemecahan |
| NetworkException | CapSkip tidak berjalan, atau host atau port-nya salah | Jalankan CapSkip dan periksa pengaturan koneksi, lihat Dokumentasi API |
Menjalankan solver di sebuah server
Loop hanya memerlukan host dan port untuk CapSkip. Dalam mode Local itu adalah 127.0.0.1 di PC yang sama. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga scraper di PC lain, VPS, atau platform hosting memanggil API yang sama. Ubah host dan port di konstruktor CapSkip dan tidak ada hal lain yang berubah. IP publik statis disarankan untuk akses jarak jauh yang stabil. Mode Server tetap berjalan di hardware Anda sendiri dan tetap tanpa meteran. Mode ini membuka port untuk semua alamat, jadi tambahkan aturan blokir Windows Firewall untuk alamat yang tidak boleh menjangkaunya. Lihat pengaturan koneksi untuk cara beralih mode.
FAQ
Bagaimana saya tahu saya diblokir atau hanya terkena rate limit?
Respons 429, terutama dengan header Retry-After, adalah rate limit, dan menunggu berhasil. Respons 403 dengan body HTML, atau halaman 200 yang menyatakan akses ditolak, adalah blokir. Sebuah 429 polos yang bertahan melewati dua kali tunggu diperlakukan sebagai blokir, sebagai aturan praktis, itulah sebabnya loop berganti proxy pada kasus itu.
Bisakah CAPTCHA yang sudah dipecahkan memperbaiki blokir?
Tidak. Blokir menolak alamat atau fingerprint Anda, dan token tidak membuktikan apa pun tentang keduanya. Loop melewati pemecah saat terjadi blokir dan berganti proxy sebagai gantinya.
Mengapa loop tidak memecahkan halaman challenge Cloudflare?
Metode Turnstile memerlukan dua nilai dari challenge, dan permintaan biasa tidak menerimanya. Loop menyerahkan kasus itu ke fungsi yang Anda sediakan, misalnya yang menjalankan browser sungguhan, dan menjaga classifier tetap jujur tentang apa yang bisa dilihatnya.
Bisakah ini dijalankan terhadap CapSkip di mesin lain?
Bisa. Pakai mode Server, arahkan konstruktor CapSkip ke host dan port itu, dan loop bekerja seperti pada satu PC. Pemecah tetap berjalan di hardware Anda.
Versi singkatnya
Putuskan CAPTCHA atau blokir lebih dulu, bertindak kemudian: pecahkan CAPTCHA, tunggu rate limit berlalu, berganti proxy saat terblokir, dan berhenti pada apa pun yang unknown. Dengan CapSkip di komputer Anda sendiri, penghematan dari melewati pemecahan yang pasti gagal adalah waktu, dan penghematan dari tidak berganti proxy tanpa perlu adalah alamat. Untuk gambaran yang lebih luas tentang melewati halaman CAPTCHA, pendekatan bypass captcha adalah memecahkan yang bisa dipecahkan dan menghindari sisanya. Untuk SDK Python itu sendiri, lihat Halaman pemecah CAPTCHA Python.
