Cara Mendeteksi Jenis CAPTCHA pada Halaman dan Memecahkannya (Python)

Untuk mendeteksi jenis CAPTCHA pada sebuah halaman, baca HTML halaman untuk menemukan skrip dan container widget, ambil sitekey dari keduanya, lalu petakan keluarganya ke metode CapSkip yang memecahkannya. Kotak reCAPTCHA v2, skrip v3, dan widget Turnstile memerlukan tiga panggilan berbeda dengan opsi yang berbeda, dan tebakan yang salah menghabiskan satu pemecahan penuh dan memberi Anda token yang tidak bisa dipakai. Panduan ini membangun detektor sepanjang sekitar 175 baris dalam Python yang mengembalikan jenis, metode, dan argumennya, atau menyatakan dengan jelas bahwa ia tidak bisa menentukan. CapSkip berjalan di PC Windows Anda sendiri tanpa biaya per pemecahan, jadi skrip yang memeriksa seratus halaman tidak menambah biaya apa pun.
Apa yang Anda butuhkan
- CapSkip yang berjalan di PC Windows, dengan pemecah yang Anda perlukan diaktifkan. Lihat panduan penyiapan untuk menjalankannya pertama kali.
- Python 3.10 atau lebih baru. Kode di bawah dijalankan pada Python 3.14.0 dan 3.11.7 dengan requests 2.33.0 dan 2.34.2 serta capskip 1.3.0.
- Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
- Halaman milik Anda atau yang boleh Anda otomatisasi, yang memiliki CAPTCHA di dalamnya.
- 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 skrip di PC lain, VPS, atau platform hosting dapat menjangkaunya melalui API. Keduanya diatur di pengaturan koneksi.
Cara pengujiannya: detektor dijalankan pada 66 halaman tersimpan dan setiap halaman mengembalikan hasil yang diharapkan. Dari jumlah itu, 59 dibuat dengan tangan (sekitar 40 mengikuti dokumentasi vendor, sisanya kasus tepi yang saya buat sendiri) dan 7 adalah snapshot halaman publik yang diambil pada 29 Oktober 2026. Pemecahan dijalankan melalui paket capskip yang asli terhadap mock CapSkip yang meniru pemeriksaan input aplikasi. Aplikasi yang asli tidak berjalan, jadi tidak ada angka di bawah ini yang merupakan waktu pemecahan.
Mengapa Anda mendeteksi jenis CAPTCHA sebelum memecahkannya
CapSkip punya metode berbeda untuk setiap keluarga: recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly_captcha, dan normal untuk gambar di SDK, ditambah hcaptcha di HTTP API, yang belum dibungkus oleh SDK. Di dalam reCAPTCHA, opsinya juga penting. Invisible, Enterprise, dan v3 adalah flag pada panggilan yang sama. Kirim yang salah dan kemungkinan terbaiknya adalah job yang gagal, yang terburuk adalah token yang ditolak situs tanpa petunjuk apa pun mengapa. Saya tidak bisa memeriksa mana yang dilakukan aplikasi asli untuk versi reCAPTCHA yang salah. Bagaimanapun, pemecahan itu sudah terpakai.
Kesalahan lain gagal dengan cepat dan jelas. Membaca sitekey dari halaman yang merender reCAPTCHA secara eksplisit menghasilkan kata explicit, dan CapSkip menolaknya dengan ERROR_GOOGLEKEY. Panduan ERROR_GOOGLEKEY dan ERROR_PAGEURL membahas error itu dari sisi sebaliknya. Agar dapat mendeteksi jenis CAPTCHA dengan andal, detektor harus memeriksa sinyal dalam urutan tetap, dan urutan itu menghindari kedua jenis kesalahan. Detektor juga hal pertama yang perlu dibuat sebelum scraper yang menemui lebih dari satu situs. Jika Anda ingin tahu latar belakang tiap jenis, lihat apa itu CAPTCHA untuk penjelasan tiap keluarga tanpa kode.
Langkah 1: instal dan ambil halaman
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
Untuk mendeteksi jenis CAPTCHA dari teks, detektor hanya memerlukan dua input: HTML halaman dan alamatnya. Ambil halaman dengan requests Session dan simpan Session itu, karena langkah CAPTCHA gambar nanti mengunduh gambarnya dengan cookie yang sama. Pengambilan requests biasa menampilkan apa yang dikirim server sebelum JavaScript apa pun dijalankan. Halaman demo reCAPTCHA dari Google, demo Turnstile dari Cloudflare, dan demo hCaptcha semuanya memuat widget-nya dalam HTML itu. Halaman yang menambahkan widget dengan JavaScript tidak demikian, dan bagian “Saat HTML tidak menunjukkan apa-apa” membahas kasus itu.
Langkah 2: tulis fungsi bantu
Simpan semuanya dari sini sampai Langkah 9 dalam satu file bernama detect.py, blok demi blok. Blok pertama mengimpor yang diperlukan dan menulis empat pembaca kecil. Masing-masing mengembalikan None jika tidak ada yang cocok, sehingga atribut yang hilang tidak pernah memunculkan exception.
import base64, html as htmllib, re, time
from types import SimpleNamespace as NS
from urllib.parse import urljoin
import requests
SITEKEY_JS = r"sitekey['\"]?\s*:\s*['\"]([^'\"]+)" # sitekey: '...' inside a render() call
def grab(pattern, text): # first capture group that matched, or None
m = re.search(pattern, text or "", re.I | re.S)
return next(g for g in m.groups() if g is not None) if m else None
def tag(cls, h): # opening tag with CSS class cls (and not g-recaptcha-response)
pat = r'(<\w+[^>]*\sclass\s*=\s*["\'][^"\']*(?<![\w-])%s(?![\w-])[^"\']*["\'][^>]*>)'
return grab(pat % cls, h)
def attr(t, name): # attribute value from a tag string, either quote style, HTML-unescaped
v = grab(r'\s%s\s*=\s*(?:"([^"]*)"|\'([^\']*)\')' % name, t)
return None if v is None else htmllib.unescape(v)
def render_opts(call, js): # first 400 characters of the options in call(el, {...})
return grab(call + r"\([^,]+,\s*\{(.{0,400})", js)Fungsi grab mengembalikan capture group pertama yang cocok. Fungsi tag menemukan tag pembuka berdasarkan class CSS dan menolak class yang hanya memuat namanya di dalam nama yang lebih panjang, sehingga class bernama cf-turnstile-response bukan widget Turnstile. Fungsi attr membaca atribut yang ditulis dengan kedua gaya tanda kutip dan melepas escape entitas HTML, yang penting untuk challenge yang disimpan di dalam atribut. Atribut tanpa tanda kutip tidak dibaca.
Blok kedua mengubah sebuah halaman menjadi sesuatu yang dapat dipakai bersama oleh pemeriksaan. Blok ini menghapus komentar dan blok style, sehingga aturan CSS yang menyebut class widget tidak bisa cocok, dan mengumpulkan alamat skrip serta JavaScript inline secara terpisah. Dua fungsi bantu terakhir membangun dua jawaban yang mungkin: sebuah hit, atau jawaban needs_browser.
def parse(html, url):
h = re.sub(r"<!--.*?-->|<style\b.*?</style>", "", html, flags=re.S | re.I)
srcs = " ".join(re.findall(r'<script\b[^>]*?\ssrc\s*=\s*["\']([^"\']+)', h, re.I))
js = "\n".join(re.findall(r"<script\b[^>]*>(.*?)</script>", h, re.S | re.I))
return NS(h=h, srcs=htmllib.unescape(srcs), js=js, url=url)
def hit(kind, method, **args):
return {"type": kind, "method": method, "args": args}
def browser(kind, method, p, why): # the static HTML is not enough, so do not guess
return {"type": kind, "method": method, "args": {"url": p.url}, "needs_browser": why}Langkah 3: hCaptcha, dan yang tidak bisa dipecahkan CapSkip
Mulailah dengan halaman yang jawaban benarnya adalah tidak. CapSkip tidak memecahkan FunCaptcha atau Arkose, dan GeeTest v4 berada di luar metode GeeTest, yang ditujukan untuk v3. Jika detektor melewati semua itu, halaman dengan kotak Arkose akan jatuh ke pemeriksaan berikutnya dan bisa dikembalikan sebagai sesuatu yang bukan dirinya. Karena itu pemeriksaan ini dijalankan pertama dan mengembalikan hasil tanpa metode.
Blok yang sama menangani pemeriksaan halaman penuh Cloudflare, interstisial “Just a moment”. Halaman itu bukan widget Turnstile. Metode turnstile membutuhkan dua nilai darinya, data dan pagedata, dan pengambilan biasa tidak memilikinya. Detektor mencari opsi challenge inline pada halaman dan menjawab needs_browser. Panduan cf_clearance membahas apa yang terjadi setelah halaman challenge berhasil dilewati.
hCaptcha diperiksa berikutnya, sebelum reCAPTCHA. CapSkip memecahkannya, baik widget-nya berupa checkbox, invisible, maupun Enterprise, tetapi situs yang pindah dari reCAPTCHA ke hCaptcha sering mempertahankan class g-recaptcha pada widget-nya, dan hanya skrip hCaptcha yang membedakan keduanya. Pemeriksaan ini membaca sitekey dari widget atau dari panggilan hcaptcha.render, dan menambahkan invisible bila widget memiliki data-size yang diatur ke invisible. Halaman Enterprise yang memberikan nilai rqdata ke widget lewat hcaptcha.setData justru mendapat needs_browser, karena nilai itu diterbitkan untuk satu challenge dan cepat kedaluwarsa.
UNSUPPORTED = {"arkose": r"arkoselabs\.com/v2/|funcaptcha\.com/fc/", "geetest_v4": r"gt4\.js|initGeetest4"}
def unsupported(p): # CapSkip cannot solve these: say so instead of misleading the reader
for kind, pat in UNSUPPORTED.items():
if re.search(pat, p.srcs + " " + p.js):
return {"type": kind, "method": None, "args": {}, "unsupported": True}
def cf_challenge(p): # the full-page "Just a moment..." interstitial, not a widget
if re.search(r"_cf_chl_opt\s*=\s*\{", p.js):
why = "data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time"
return browser("turnstile_challenge", "turnstile", p, why)
def hcaptcha(p): # before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
if not re.search(r"hcaptcha\.com/1/api\.js", p.srcs + " " + p.js):
return None
if re.search(r"\.setData\s*\(|rqdata", p.js): # Enterprise rqdata is issued per challenge
return browser("hcaptcha_enterprise", "hcaptcha", p, "rqdata is issued for one challenge at run time")
w = tag("h-captcha", p.h) or tag("g-recaptcha", p.h)
key = attr(w, "data-sitekey") or grab(SITEKEY_JS, render_opts(r"hcaptcha\.render", p.js))
if not key:
return browser("hcaptcha", "hcaptcha", p, "no sitekey in the static HTML")
args = {"sitekey": key, "url": p.url}
if attr(w, "data-size") == "invisible":
args["invisible"] = 1
return hit("hcaptcha", "hcaptcha", **args)Langkah 4: reCAPTCHA, dalam urutan yang benar
reCAPTCHA memerlukan kehati-hatian paling besar karena beberapa varian CapSkip berbagi dua nama skrip. Urutan pertanyaan itulah yang memisahkannya. Pertama nama skripnya: enterprise.js berarti flag Enterprise, api.js tidak. Lalu parameter render pada alamat skrip. Kunci setelah render= berarti v3, tetapi kata explicit setelah render= berarti halaman v2 yang merender widget-nya dari kode, dan kata itu tidak boleh dipakai sebagai kunci. Lalu markup. Tombol dengan class g-recaptcha dan atribut data-action adalah binding tombol v3 dari Google. Tombol tanpa data-action, atau elemen apa pun dengan data-size diatur ke invisible, adalah widget v2 invisible. Selain itu, apa pun dengan class tersebut adalah checkbox.
def recaptcha(p): # Enterprise flag first, then v3 (render=KEY), then v2
m = re.search(r"/recaptcha/(api|enterprise)\.js(\?\S*)?", p.srcs)
if not m:
return None
ent = {"enterprise": 1} if m.group(1) == "enterprise" else {}
render = grab(r"[?&]render=([^&\s]+)", m.group(2)) # a v3 sitekey, or "explicit" (a v2 page)
t, o = tag("g-recaptcha", p.h), render_opts(r"\.render", p.js)
button, action = (t or "").lower().startswith("<button"), attr(t, "data-action")
v3 = bool(render and render != "explicit") or bool(button and action)
key = render if render != "explicit" else None
key = key or attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
if not key:
return browser("recaptcha", "recaptcha", p, "no sitekey in the static HTML (set by JavaScript)")
small = attr(t, "data-size") == "invisible" or grab(r"size['\"]?\s*:\s*['\"](invisible)", o)
invisible = not v3 and bool(button or small)
action = action or grab(r"execute\(\s*[^,)]+,\s*\{\s*['\"]?action['\"]?\s*:\s*['\"]([^'\"]+)", p.js)
args = {"sitekey": key, "url": p.url, **ent}
if v3:
args.update(version="v3", **({"action": action} if action else {}))
if invisible:
args["invisible"] = 1
kind = "recaptcha_" + ("v3" if v3 else "v2_invisible" if invisible else "v2")
return hit(kind + ("_enterprise" if ent else ""), "recaptcha", **args)Setiap hasil dipetakan ke panggilan yang sudah Anda kenal. Checkbox adalah solver.recaptcha dengan sitekey dan URL halaman. Invisible menambahkan invisible=1, Enterprise menambahkan enterprise=1, dan v3 menambahkan version="v3" serta action. Nama action berasal dari atribut data-action atau dari panggilan execute pada halaman; jika keduanya tidak ada di HTML, argumen itu dihilangkan dan CapSkip memakai nilai defaultnya, yaitu verify. Lihat halaman pemecah reCAPTCHA untuk setiap varian yang ditanganinya.
Langkah 5: Turnstile
Widget Turnstile memerlukan dua hal pada halaman: api.js dari Cloudflare dan sebuah sitekey. Kuncinya berada di atribut data-sitekey pada sebuah div dengan class cf-turnstile, atau di panggilan turnstile.render. Pemeriksaan ini mensyaratkan skripnya, sehingga nama class nyasar di halaman yang tidak terkait tidak bisa memicunya. Pemeriksaan ini juga membaca data-action dan data-cdata bila ada, lalu meneruskannya. Halaman pemecah Turnstile mencantumkan opsinya.
def turnstile(p): # widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)
if "challenges.cloudflare.com/turnstile/v0/api.js" not in p.srcs:
return None
t, o = tag("cf-turnstile", p.h), render_opts(r"turnstile\.render", p.js)
key = attr(t, "data-sitekey") or grab(SITEKEY_JS, o)
if not key:
return browser("turnstile", "turnstile", p, "no sitekey in the static HTML")
action = attr(t, "data-action") or grab(r"\baction['\"]?\s*:\s*['\"]([^'\"]+)", o)
cdata = attr(t, "data-cdata") or grab(r"\bcData['\"]?\s*:\s*['\"]([^'\"]+)", o)
extra = {k: v for k, v in (("action", action), ("data", cdata)) if v}
return hit("turnstile", "turnstile", sitekey=key, url=p.url, **extra)Langkah 6: GeeTest dan ALTCHA
GeeTest v3 memberi Anda dua nilai, dan salah satunya adalah jebakan. Nilai gt tetap untuk situs dan bisa berada di halaman. Nilai challenge hanya berlaku sekali, dan situs mengambilnya dari endpoint register saat dijalankan. Jika halaman memuat keduanya, detektor mengembalikannya. Jika halaman hanya memuat panggilan initGeetest, detektor menjawab needs_browser, karena challenge yang ditemukan di HTML lama sudah mati.
ALTCHA mengubah nama atributnya antargenerasi. Versi 1 dan 2 memakai challengeurl, atau challengejson untuk challenge inline. Versi 3 dan seterusnya memakai satu atribut bernama challenge, yang menerima URL atau data challenge. Pemeriksaan ini membaca ketiganya dan menggabungkan URL relatif dengan alamat halaman.
def geetest(p): # v3 only: gt is static, challenge comes from the register endpoint
if not (re.search(r"initGeetest\s*\(", p.js) or re.search(r"/gt(\.[\d.]+)?\.js", p.srcs)):
return None
gt = grab(r"\bgt['\"]?\s*:\s*['\"]([0-9a-f]{32})", p.js)
challenge = grab(r"\bchallenge['\"]?\s*:\s*['\"]([0-9a-f]{32,34})", p.js)
if not (gt and challenge):
return browser("geetest", "geetest", p, "gt and challenge come from the register endpoint")
return hit("geetest", "geetest", gt=gt, challenge=challenge, url=p.url)
def altcha(p): # challengeurl / challengejson (v1, v2) or challenge (v3+)
t = grab(r"(<altcha-widget\b[^>]*>)", p.h)
if not t:
return None
c = attr(t, "challenge")
as_json = attr(t, "challengejson") or (c if (c or "").lstrip().startswith("{") else None)
as_url = attr(t, "challengeurl") or (None if as_json else c)
if as_json:
return hit("altcha", "altcha", url=p.url, challenge_json=as_json)
if as_url:
return hit("altcha", "altcha", url=p.url, challenge_url=urljoin(p.url, as_url))
return browser("altcha", "altcha", p, "no challenge attribute")Langkah 7: Capy, CaptchaFox, dan Friendly Captcha
Ketiganya mudah ditemukan dan mudah salah sedikit. Capy tidak memerlukan div widget untuk pemeriksaan ini. Kuncinya diawali PUZZLE_ dan berada di parameter k pada alamat skrip widget, dan host selain default diteruskan sebagai api_server. CaptchaFox hadir dalam dua build. Yang biasa dimuat dari cdn.captchafox.com. Yang lain dibundel oleh platform hosting di bawah path CDN bersama, sehingga detektor mencari nama paket CaptchaFox di path itu dan bukan path-nya saja, yang juga dipakai widget lain.
Friendly Captcha adalah yang paling rumit. Versi 1 dan versi 2 memakai div yang sama dan namespace sitekey yang sama, tetapi protokolnya berbeda, dan token untuk yang salah diterima CapSkip lalu ditolak situs. Skripnya membedakan keduanya: versi 2 memuat paket @friendlycaptcha/sdk, versi 1 memuat friendly-challenge. Bila halaman tidak memberi petunjuk, detektor menolak menebak dan menjawab needs_browser.
def capy(p): # the widget script is .../puzzle/get_js/?k=PUZZLE_...
m = re.search(r"((?:https?:)?//[^\s\"']+?)/puzzle/get_js/\S*?(PUZZLE_[\w-]+)", p.srcs)
if m:
host = m.group(1) if m.group(1).startswith("http") else "https:" + m.group(1)
more = {} if host == "https://jp.api.capy.me" else {"api_server": host}
return hit("capy", "capy", sitekey=m.group(2), url=p.url, **more)
MAM = "mampkg/@mamdev/core.frontend.libs.captchafox"
def captchafox(p): # plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
t, key = tag("captchafox", p.h), grab(r"captchafox[^\"']*/api\.js\?key=([^\"'&]+)", p.srcs)
if t or key:
more = {"api_server": "https://s.uicdn.com/" + MAM + "/"} if MAM in p.srcs else {}
sitekey = attr(t, "data-sitekey") or key
if not sitekey:
return browser("captchafox", "captchafox", p, "no sitekey in the static HTML")
return hit("captchafox", "captchafox", sitekey=sitekey, url=p.url, **more)
def friendly(p): # v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
t = tag("frc-captcha", p.h)
if not t:
return None
v2_end, v1_end = attr(t, "data-api-endpoint"), attr(t, "data-puzzle-endpoint")
server = v2_end or v1_end
v2 = v2_end is not None or re.search(r"friendlycaptcha/sdk|site(\.compat)?\.min\.js", p.srcs)
v1 = v1_end is not None or re.search(r"friendly-challenge|widget(\.module)?\.min\.js", p.srcs)
if bool(v2) == bool(v1):
return browser("friendly_captcha", "friendly_captcha", p, "cannot tell v1 from v2 in the HTML")
if not attr(t, "data-sitekey"):
return browser("friendly_captcha", "friendly_captcha", p, "no sitekey in the static HTML")
more = {"api_server": server} if server else {}
return hit("friendly_captcha_" + ("v2" if v2 else "v1"), "friendly_captcha",
sitekey=attr(t, "data-sitekey"), url=p.url, version="v2" if v2 else "v1", **more)Masing-masing punya halaman pemecahnya sendiri dengan opsi yang diterima metodenya:
- Halaman pemecah Capy Puzzle: tiga nilai yang dikembalikan jawaban Capy.
- Halaman pemecah CaptchaFox: opsi yang diterima metode CaptchaFox.
- Halaman pemecah Friendly Captcha: metode Friendly Captcha.
Langkah 8: CAPTCHA gambar, sebuah tebakan
CAPTCHA gambar tidak memiliki skrip vendor untuk ditemukan, jadi pemeriksaan ini hanya tebakan dan dijalankan hanya bila tidak ada yang lain cocok. Pemeriksaan ini mencari gambar yang tag-nya menyebut captcha, dan kotak teks di tempat lain pada halaman yang tag-nya menyebut captcha atau security code. Pemeriksaan ini meleset di setiap kasus ketika markup tidak memakai kata-kata itu, dan canvas yang digambar tidak terlihat olehnya. Anggap sebuah hit sebagai kemungkinan, bukan kepastian.
def image(p): # a guess: an <img> that says captcha, next to a text box that says captcha
box = r"<input\b(?![^>]*type\s*=\s*[\"']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)"
for img in re.findall(r"<img\b[^>]*>", p.h, re.I):
if re.search(box, p.h, re.I) and re.search(r"captcha", img, re.I) and attr(img, "src"):
return hit("image", "normal", image=urljoin(p.url, attr(img, "src")))Langkah 9: pemeriksaan, detect, dan solve
Blok terakhir mendaftar pemeriksaan secara berurutan dan mengekspos detect dan solve, dengan detect_all sebagai fungsi bantu. Urutan itulah logikanya: vendor unsupported dulu, lalu halaman Cloudflare, lalu hCaptcha, lalu setiap keluarga lainnya, dan tebakan gambar hanya bila daftar tidak menemukan apa-apa. Fungsi detect mengembalikan hasil pertama dan menambahkan daftar also bila halaman memuat lebih dari satu keluarga, misalnya skrip v3 ditambah kotak Turnstile. Daftar itu menyebut keluarga, bukan widget reCAPTCHA kedua dengan kunci lain. Fungsi solve meneruskan hasil ke metode CapSkip yang tepat. Fungsi ini menolak apa pun yang unsupported atau ditandai needs_browser dengan RuntimeError, dan mengunduh gambar lewat Session Anda sendiri agar cookie-nya cocok.
SDK belum punya panggilan hCaptcha, jadi hasil hCaptcha diteruskan ke solve_hcaptcha. Fungsi ini mengirim sitekey dan alamat halaman ke in.php dengan method=hcaptcha, lalu memeriksa res.php setiap 5 detik sampai token siap, sebagaimana bagian hCaptcha dalam dokumentasi API menjelaskannya. Berikan alamat solver sebagai api, dan API key Anda sebagai key bila validasi API key diaktifkan. Hasilnya membawa token di code dan User-Agent yang dipakai untuk memperolehnya di userAgent.
CHECKS = [unsupported, cf_challenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly]
def detect_all(html, page_url):
p = parse(html, page_url)
return [r for r in (check(p) for check in CHECKS) if r] or [r for r in [image(p)] if r]
def detect(html, page_url):
found = detect_all(html, page_url)
if len(found) > 1:
found[0]["also"] = [r["type"] for r in found[1:]]
return found[0] if found else None
def solve_hcaptcha(args, api, key=None): # no SDK call yet: post to in.php, poll res.php
form = {"method": "hcaptcha", "sitekey": args["sitekey"], "pageurl": args["url"],
"invisible": args.get("invisible", 0), "data": args.get("data"), "key": key, "json": 1}
job = requests.post(api + "/in.php", data=form, timeout=30).json()
if job["status"] != 1:
raise RuntimeError(job["request"])
for _ in range(36): # a solve takes 10 to 30 seconds, so give up after 3 minutes
time.sleep(5)
q = {"key": key, "action": "get", "id": job["request"], "json": 1}
r = requests.get(api + "/res.php", params=q, timeout=30).json()
if r["request"] != "CAPCHA_NOT_READY":
if r["status"] != 1:
raise RuntimeError(r["request"])
return {"code": r["request"], "userAgent": r.get("useragent")}
raise TimeoutError("hCaptcha was not solved within 3 minutes")
def solve(info, solver, session=None, api="http://127.0.0.1:8080", key=None):
if info.get("unsupported") or info.get("needs_browser"):
raise RuntimeError(info.get("needs_browser") or info["type"] + " is not supported by CapSkip")
args = dict(info["args"])
if info["method"] == "hcaptcha":
return solve_hcaptcha(args, api, key)
if info["method"] == "normal": # fetch the image with YOUR session so the cookie matches it
src = args.pop("image")
if not src.startswith("data:"):
r = (session or requests).get(src, timeout=30)
r.raise_for_status()
src = "data:image/png;base64," + base64.b64encode(r.content).decode()
args["file"] = src
return getattr(solver, info["method"])(**args)Jalankan
Simpan ini sebagai use.py di samping detect.py dan 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. Skrip mengambil halaman Anda, mencetak apa yang ditemukan detektor, dan hanya memanggil CapSkip bila ada metode untuk dipanggil dan tidak ada yang kurang.
import requests
from capskip import CapSkip
from detect import detect, solve
URL = "https://example.com/login" # a page of yours with a CAPTCHA
HOST, PORT = "127.0.0.1", 8080 # from CapSkip's connection settings
session = requests.Session()
html = session.get(URL, timeout=30).text
info = detect(html, URL)
print(info)
if info and info["method"] and not info.get("needs_browser"):
solver = CapSkip(host=HOST, port=PORT)
result = solve(info, solver, session, api=f"http://{HOST}:{PORT}")
print(result)Pada halaman dengan checkbox reCAPTCHA v2, print pertama menampilkan jenis, metode, dan argumennya, dan print kedua menampilkan hasilnya dengan token di field code. Pada eksekusi mock, hasilnya seperti ini:
| Halaman | detect mengembalikan |
|---|---|
| reCAPTCHA v2 checkbox | type recaptcha_v2, method recaptcha, sitekey dan url |
| Widget hCaptcha | type hcaptcha, method hcaptcha, sitekey dan url |
| Halaman Cloudflare “Just a moment” | type turnstile_challenge, needs_browser diatur |
| Skrip v3 ditambah widget Turnstile | type recaptcha_v3, also mencantumkan turnstile |
Saat HTML tidak menunjukkan apa-apa: konsol browser
Beberapa halaman menambahkan widget-nya dengan JavaScript setelah dimuat. Pengambilan biasa lalu tidak menemukan CAPTCHA sama sekali, dan detektor mengembalikan None untuk halaman yang memilikinya. Dua kasus diketahui dan memang diharapkan: skrip Google atau Cloudflare yang ditambahkan lewat kode, dan sitekey yang ditetapkan lewat kode. Untuk kasus itu, buka halaman di Chrome, tekan F12, dan tempel ini ke Console. Jika Chrome memblokir penempelan, ketik allow pasting lalu tekan Enter lebih dulu. Snippet ini membaca sitekey dari resource yang sudah dimuat halaman, sehingga ia melihat apa yang dibangun browser.
(() => {
const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
for (const u of urls) {
let m;
if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
add({ type: 'turnstile', sitekey: m[1] });
else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
}
if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey); // a v3 badge also adds an invisible anchor frame
const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();Snippet itu menemukan sitekey dan flag pada halaman reCAPTCHA v2, halaman invisible, halaman v3, halaman Turnstile, dan demo hCaptcha, dan mencetak pesan pada halaman tanpa CAPTCHA. Tiga batasan: browser hanya menyimpan sekitar 250 entri resource secara default, jadi single page app yang berjalan lama bisa kehilangan entri awal, dan snippet tidak bisa membedakan Friendly Captcha versi 1 dari versi 2, serta tidak mencari skrip GeeTest, Capy, atau CaptchaFox. Pada pemeriksaan halaman penuh Cloudflare, snippet bisa menampilkan sitekey dari alamat resource, tetapi tidak nilai data dan pagedata, yang memerlukan hook pada panggilan render. Hook itu tidak diuji di sini.
Tempat deteksi gagal
| Situasi | Yang dikembalikan detect | Apa yang harus dilakukan |
|---|---|---|
| Skrip widget ditambahkan oleh JavaScript | Tidak ada | Muat halaman di browser dan jalankan snippet console |
| Sitekey ditetapkan oleh JavaScript | needs_browser | Baca kunci dari halaman yang sedang berjalan |
| Pemeriksaan halaman penuh Cloudflare | needs_browser | Baca nilainya di browser, lihat panduan cf_clearance |
| Skrip CaptchaFox, Friendly Captcha, atau Capy tanpa widget di HTML | Tidak ada | Jalankan snippet console atau baca source halaman |
| Halaman 403 atau 503 dari situs itu sendiri | Tidak ada | Cetak kode status lebih dulu: halaman blokir tidak memuat CAPTCHA untuk ditemukan |
| Arkose, GeeTest v4 | unsupported | CapSkip tidak bisa memecahkan ini |
| hCaptcha Enterprise yang mengatur rqdata | needs_browser | Baca rqdata baru dari halaman live untuk setiap tugas dan berikan ke solve_hcaptcha sebagai data |
| Friendly Captcha tanpa petunjuk skrip | needs_browser | Periksa skrip mana yang dimuat halaman |
| CAPTCHA gambar tanpa kata captcha | Tidak ada | Tambahkan aturan Anda sendiri untuk situs itu |
Kesalahan umum
| Kesalahan | Penyebab | Perbaiki |
|---|---|---|
| ERROR_GOOGLEKEY | Kuncinya adalah kata explicit, atau kunci yang bukan kunci reCAPTCHA | Baca kunci dari widget, bukan dari parameter render |
| ERROR_CAPTCHA_UNSOLVABLE | Setiap job yang gagal terbaca seperti ini, termasuk versi reCAPTCHA yang salah, dan pesannya tidak menyebut penyebabnya | Periksa dulu jenis yang terdeteksi, lalu lihat Dokumentasi API |
| ValidationException | Opsi yang tidak diterima metode, seperti action pada panggilan v2 | Gunakan argumen yang dikembalikan detektor |
| NetworkException | CapSkip tidak berjalan, atau host atau port-nya salah | Jalankan CapSkip dan periksa pengaturan koneksi |
| RuntimeError dari solve | Hasilnya unsupported atau needs_browser, atau CapSkip menolak job hCaptcha dan pesannya adalah kode error-nya | Tangani dua kasus pertama sebelum memanggil solve, dan periksa sitekey serta alamat halaman untuk kasus ketiga |
Menjalankan solver di sebuah server
Detektor hanya memerlukan dua hal dari CapSkip: host dan port. Dalam mode Local itu adalah 127.0.0.1 dan PC Anda sendiri. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga detektor yang berjalan di PC lain, VPS, atau platform hosting dapat memanggilnya melalui API yang sama. Ubah HOST dan PORT di bagian atas use.py, dan tidak ada hal lain dalam kode yang berubah. IP publik statis disarankan untuk akses jarak jauh yang stabil. Mode Server tetap berjalan di hardware Anda sendiri dan tetap tanpa meteran. Perhatikan bahwa mode Server 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
Bisakah saya mengetahui CAPTCHA apa yang dipakai sebuah situs tanpa kode?
Bisa. Buka DevTools, masuk ke tab Network, muat ulang halaman, dan lihat alamat skripnya. Skrip google.com/recaptcha atau recaptcha.net adalah reCAPTCHA, challenges.cloudflare.com adalah Turnstile, dan js.hcaptcha.com adalah hCaptcha. Snippet Console di atas melakukan pencarian yang sama untuk Anda.
Mengapa detect mengembalikan needs_browser alih-alih menebak?
Karena tebakan yang salah menyia-nyiakan satu pemecahan penuh dan mengembalikan token yang ditolak situs tanpa petunjuk mengapa. Bila HTML statis tidak memuat sitekey, challenge GeeTest, atau versi Friendly Captcha, menyatakannya lebih singkat daripada job yang gagal.
Apakah CapSkip memecahkan hCaptcha?
Ya. CapSkip memecahkan widget hCaptcha checkbox, invisible, dan Enterprise melalui HTTP API-nya dengan method=hcaptcha, sebagaimana Dokumentasi API menjelaskannya. SDK Python belum punya panggilan hCaptcha, jadi solve_hcaptcha di atas mengirim job ke in.php sendiri. FunCaptcha, Arkose, dan GeeTest v4 masih belum didukung, dan detektor melaporkannya sebagai unsupported agar skrip dapat melewati halaman itu alih-alih gagal di sana.
Bisakah platform hosting memakai detektor dengan CapSkip saya?
Bisa, lewat mode Server. Atur CapSkip untuk mendengarkan di jaringan atau IP publik Anda, atur HOST dan PORT di use.py ke alamat itu, dan detektor serta panggilan solve bekerja sama seperti pada satu PC. Pemecah tetap berjalan di hardware Anda.
Versi singkatnya
Deteksi jenis CAPTCHA dulu, baru pecahkan: baca skrip dan markup dalam urutan tetap, serahkan argumen ke metode yang tepat, dan perlakukan None, needs_browser, dan unsupported sebagai jawaban, bukan error. Dengan CapSkip di komputer Anda sendiri, tidak ada meteran yang berjalan saat Anda memeriksa halaman, dan itulah yang membuat pemecah captcha tanpa batas praktis untuk crawl yang menemui situs baru setiap beberapa menit. Untuk SDK Python itu sendiri, lihat Halaman pemecah CAPTCHA Python.
