Cara Memecahkan Friendly Captcha di Python dari Tag Script

Untuk memecahkan Friendly Captcha di Python, baca data-sitekey dari elemen frc-captcha, temukan tag script yang memuat widget, bedakan v1 dari v2 berdasarkan skrip itu, lalu berikan sitekey, URL halaman, versi, dan alamat lengkap skrip ke metode friendly_captcha milik CapSkip. Anda akan menerima token untuk dikirim di frc-captcha-solution pada v1 atau frc-captcha-response pada v2. Tag script adalah bagian yang menentukan. Friendly Captcha menghadirkan dua protokol yang tidak berhubungan dengan satu nama dan satu format sitekey, dan memecahkan versi yang salah menghasilkan token yang tampak valid tetapi diam-diam ditolak. CapSkip menambahkan Friendly Captcha pada versi 1.4.0. Panduan ini membahas cara membaca halaman, pemanggilannya, pengirimannya, dan menjalankan banyak pemecahan sekaligus dengan AsyncCapSkip.
Apa yang Anda butuhkan
- CapSkip 1.4.0 atau lebih baru yang berjalan di komputer Windows. Rilis itu menambahkan Friendly Captcha, bersama CaptchaFox dan Capy Puzzle.
- Python 3.10 atau lebih baru dan paket capskip versi 1.3.0 atau lebih baru, rilis pertama yang memiliki friendly_captcha. Contoh kodenya juga memakai requests untuk mengambil halaman dan mengirim form.
- URL halaman yang menampilkan widget. Sitekey, alamat skrip, dan nama field semuanya diambil dari HTML halaman itu, dan Langkah 1 menunjukkan letaknya.
- Sebuah alamat untuk pemecah CAPTCHA. Mode Local menjawab di 127.0.0.1 hanya untuk perangkat itu; mode Server mendengarkan di alamat jaringan atau IP publik Anda sehingga skrip di mesin lain dapat memanggilnya melalui API. Keduanya ada di pengaturan koneksi, dan Langkah 4 membahas kapan harus beralih.
# pip install capskip requests pip install "capskip>=1.3.0" requests
Jangan hapus tanda kutipnya. Di cmd, tanda lebih besar tanpa tanda kutip adalah redirect: pip menginstal capskip versi apa pun yang ditemukannya, tanpa pemeriksaan versi, dan menulis output-nya ke file bernama 1.3.0. PowerShell meneruskan argumen itu apa adanya, tetapi tanda kutip berfungsi di semua shell.
Langkah 1: baca widget dan tag script-nya
Kedua versi merender elemen yang sama, yaitu sebuah div dengan class frc-captcha dan atribut data-sitekey. Jadi elemen itu memberi Anda sitekey, tetapi tidak memberi tahu apa pun tentang protokolnya. Versinya ada di tag script yang memuat widget, karena v1 dan v2 adalah paket berbeda dengan nama file yang berbeda:
<!-- v2: the @friendlycaptcha/sdk package --> <div class="frc-captcha" data-sitekey="YOUR_SITEKEY"></div> <script type="module" src="https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk/site.min.js" async defer></script> <script nomodule src="https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk/site.compat.min.js" async defer></script> <!-- v1: the friendly-challenge package --> <div class="frc-captcha" data-sitekey="YOUR_SITEKEY"></div> <script type="module" src="https://cdn.jsdelivr.net/npm/friendly-challenge/widget.module.min.js" async defer></script> <script nomodule src="https://cdn.jsdelivr.net/npm/friendly-challenge/widget.min.js" async defer></script>
Sebagian besar situs mengunci nomor versi di dalam path itu, dan itu tidak mengubah apa pun di sini. Pustaka standar Python sudah cukup untuk membaca semuanya. Parser ini menyimpan atribut elemen widget dan alamat setiap skrip yang bukan cadangan nomodule, yang di-resolve relatif terhadap URL halaman:
# pip install capskip requests
from html.parser import HTMLParser
from urllib.parse import urljoin
import requests
class FriendlyPage(HTMLParser):
"""Collects the frc-captcha element and the page's script tags."""
def __init__(self, page_url):
super().__init__()
self.page_url = page_url
self.widget = {}
self.scripts = []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if "frc-captcha" in (a.get("class") or "").split():
self.widget = a
# Skip nomodule fallbacks; keep full addresses, never relative.
if tag == "script" and a.get("src") and "nomodule" not in a:
self.scripts.append(urljoin(self.page_url, a["src"]))
session = requests.Session()
page = FriendlyPage("https://example.com/signup")
page.feed(session.get(page.page_url, timeout=30).text)Pemanggilan urljoin itu bukan tanpa alasan. Widget yang di-host sendiri bisa memiliki src relatif, seperti /js/site.min.js, sedangkan CapSkip membutuhkan alamat yang bisa diambilnya: pada v2, CapSkip memuat skrip yang sama di browser miliknya sendiri untuk menjalankan pemecahan. Jika Anda hanya memberikan path mentahnya, widget tidak akan pernah dimuat.
Langkah 2: pilih versi dan panggil friendly_captcha
CapSkip menentukan versi dengan urutan tetap dan berhenti pada jawaban pertama: versi yang Anda berikan, lalu alamat skrip yang Anda berikan sebagai module_script, lalu default v1. Langkah terakhir itulah jebakannya. Skrip yang versinya tidak bisa dibaca CapSkip, seperti /assets/app.4f2a.js hasil bundling, dipecahkan sebagai v1 tanpa peringatan apa pun, lalu situs v2 menolak setiap token.
Jadi tentukan versinya di kode Anda sendiri, tempat Anda bisa menolak untuk menebak. Nama paket di alamat CDN tidak ambigu. Build yang di-host sendiri sering menghilangkan nama paket itu tetapi mempertahankan nama file-nya, seperti yang dilakukan plugin WordPress resmi: site.min.js berarti v2, dan widget.module.min.js atau widget.min.js berarti v1. Atribut milik widget itu sendiri adalah pilihan terakhir, karena kedua versi menamai opsinya secara berbeda:
from urllib.parse import urlparse
V2_FILES = {"site.min.js", "site.compat.min.js"}
V1_FILES = {"widget.module.min.js", "widget.min.js", "widget.polyfilled.min.js"}
def friendly_version(page):
# Package names are unambiguous, so check them first.
for src in page.scripts:
if "@friendlycaptcha/sdk" in src:
return "v2", src
if "friendly-challenge" in src:
return "v1", src
# Self-hosted builds usually keep the file name. Themes ship their own
# site.min.js too, so only trust a path that says friendly.
for src in [s for s in page.scripts if "friendly" in s.lower()]:
name = urlparse(src).path.rsplit("/", 1)[-1]
if name in V2_FILES:
return "v2", src
if name in V1_FILES:
return "v1", src
# Last resort: v2 and v1 name their widget options differently.
if {"data-api-endpoint", "data-form-field-name"} & page.widget.keys():
return "v2", None
if {"data-puzzle-endpoint", "data-solution-field-name"} & page.widget.keys():
return "v1", None
raise RuntimeError("v1 or v2? Read the page and set it by hand.")Pemeriksaan nama file hanya memercayai path yang menyebut friendly, karena sebuah tema bisa memuat site.min.js miliknya sendiri yang sama sekali tidak berhubungan dengan widget.
Lalu lakukan pemanggilannya. Kirim versi yang Anda temukan, ditambah alamat skrip jika Anda memilikinya. Versi yang eksplisit selalu menang, dan pada v2 alamat itu memberi tahu CapSkip untuk memecahkan dengan build persis yang dimuat situs. Nilai None tidak ikut dikirim dalam permintaan.
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
version, script = friendly_version(page)
result = solver.friendly_captcha(
page.widget["data-sitekey"],
"https://example.com/signup",
version=version,
module_script=script,
# data-api-endpoint="eu" (v2) or data-puzzle-endpoint (v1).
api_server=page.widget.get("data-api-endpoint")
or page.widget.get("data-puzzle-endpoint"),
)
print(result["token"][:40]) # v2 tokens start with AQQA.Baris api_server menangani situs yang memakai endpoint EU milik Friendly Captcha. Sitekey dari situs EU tetap mendapat token dari endpoint global, jadi memecahkannya di sana baru gagal saat verifikasi milik situs itu sendiri, kegagalan diam-diam yang sama seperti pada versi. Jika kedua atribut itu tidak ada, nilainya None dan CapSkip memakai endpoint global.
Hasilnya adalah dict biasa. result["token"] adalah string yang harus dikirim, result["code"] berisi string yang sama untuk skrip yang diporting dari pemecah CAPTCHA lain, dan result["captchaId"] adalah referensi CapSkip untuk pekerjaan tersebut. Versi selain v1, v2, 1 atau 2, sitekey yang kosong, atau opsi yang tidak diterima metode ini akan memunculkan ValidationException sebelum apa pun dikirim.
Langkah 3: kirim token ke field yang dipakai widget
Nama field adalah hal kedua yang berbeda antar versi, dan situs bisa mengganti namanya pada elemen widget:
import requests
# A renamed field wins; otherwise the default for the version.
field = (
page.widget.get("data-form-field-name") # v2 rename
or page.widget.get("data-solution-field-name") # v1 rename
or ("frc-captcha-response" if version == "v2" else "frc-captcha-solution")
)
# session is the requests.Session that fetched the page.
resp = session.post(
"https://example.com/signup",
data={"email": "YOUR_EMAIL", field: result["token"]},
# Browsers send the page as Referer; some servers refuse a post without it.
headers={"Referer": page.page_url},
timeout=30,
)
print(resp.status_code)Satu kasus khusus di v1: data-solution-field-name yang diisi satu tanda hubung berarti widget sama sekali tidak menulis field tersembunyi, dan skrip milik situs itu sendiri mengirim token dengan cara lain. Tiru permintaan itu, seperti dijelaskan di bawah.
Kirim ke alamat mana pun yang ditunjuk atribut action milik form, dan sertakan field lain dari form itu, termasuk field tersembunyi seperti token CSRF. Mengambil halaman dan mengirim form melalui satu requests.Session akan menjaga cookie situs, yang biasanya dibutuhkan token CSRF itu. Kirim juga URL halaman sebagai Referer: requests tidak mengirim header Referer maupun Origin, dan sebagian framework, termasuk Django, menolak form post HTTPS yang tidak memiliki keduanya, meskipun cookie dan tokennya sudah benar.
Kirim token di body dengan data=, jangan pernah di URL dengan params=. Token v2 berukuran kira-kira enam kilobyte, cukup untuk melampaui batas panjang URL di server, sedangkan token v1 terdiri dari empat bagian yang dipisahkan titik dan panjangnya beberapa ratus karakter. Teruskan apa adanya, tanpa dipangkas atau di-encode ulang. Token hanya berlaku untuk satu pengiriman: verifikasi Friendly Captcha menolak respons yang sudah pernah dipakai atau sudah kedaluwarsa, jadi pecahkan ulang untuk setiap form.
Sebagian situs mengirim form dengan JavaScript dan body JSON, bukan lewat form post. Jika pengiriman gagal padahal nama field-nya benar, buka DevTools, kirim sekali secara manual, lalu salin persis apa yang dikirim halaman itu.
Langkah 4: banyak form sekaligus, dan di mana pemecah CAPTCHA berjalan
AsyncCapSkip di Python adalah klien asinkron sungguhan yang dibangun di atas httpx, bukan alias dari klien yang memblokir, sehingga satu event loop bisa menjalankan banyak pemecahan sekaligus. Batasi jumlahnya dengan semaphore agar yang berjalan tidak pernah melebihi jumlah yang dikerjakan CapSkip secara bersamaan. Pengaturan Max. Threads di bagian Friendly Captcha pada CapSkip bernilai default 10.
import asyncio
from capskip import AsyncCapSkip
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
limit = asyncio.Semaphore(10) # match Friendly Captcha Max. Threads
async def solve(sitekey, url, version, script):
async with limit:
r = await solver.friendly_captcha(
sitekey, url, version=version, module_script=script)
return r["token"]
async def main(jobs):
# jobs: (sitekey, url, version, script) tuples from Step 2
return await asyncio.gather(*(solve(*j) for j in jobs))Waktu pemecahan akan bervariasi. Friendly Captcha menetapkan jumlah kerja per permintaan dan menaikkannya untuk alamat yang sudah sering dilihatnya, dan CapSkip memecahkan setiap widget v2 di browser sungguhan. Itulah sebabnya metode ini melakukan polling dengan recaptchaTimeout, 300 detik secara default, bukan defaultTimeout 120 detik. CapSkip juga punya batasnya sendiri. Di pengaturan Friendly Captcha-nya, sebuah tugas boleh menunggu 250 detik untuk mendapat thread yang kosong, lalu menghabiskan 120 detik untuk memecahkan sebelum CapSkip menggagalkannya. Jadi pemecahan yang lambat membutuhkan Row Timeout yang lebih tinggi, dan timeout milik SDK sendiri, 300 detik secara default atau timeout= per pemanggilan, harus mencakup waktu itu ditambah berapa pun lama tugas menunggu thread. Semaphore di atas menjaga waktu tunggu itu tetap mendekati nol. Jika pemecahan melambat selama proses yang panjang, tingkat kesulitan yang naik pada satu alamat biasanya menjadi penyebabnya. Berikan proxy per permintaan sebagai dict dengan key type dan uri, atau konfigurasikan pool proxy di CapSkip agar setiap pemecahan tidak berasal dari alamat yang sama.
Contoh kode memakai 127.0.0.1 karena itu benar ketika skrip Anda dan pemecah CAPTCHA berada di mesin yang sama. Begitu skrip berjalan di tempat lain, seperti VPS, container, atau CI runner, loopback menunjuk ke mesin yang salah dan pemanggilan pertama memunculkan NetworkException. Alihkan CapSkip ke mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda, sehingga semua lingkungan itu dapat menjangkaunya melalui API yang sama. Gunakan IP publik statis jika rutenya melewati internet, dengan aturan firewall untuk alamat yang Anda harapkan. Perangkatnya tetap milik Anda sendiri dan tetap tanpa kuota. Klien tidak membaca variabel lingkungan dengan sendirinya, jadi baca CAPSKIP_HOST di kode Anda lalu teruskan ke klien, seperti yang dilakukan contoh lengkap di bawah.
Contoh lengkap yang berfungsi
# pip install capskip requests
import os
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse
import requests
from capskip import CapSkip
from capskip.exceptions import CapSkipError, ValidationException
PAGE_URL = "https://example.com/signup"
V2_FILES = {"site.min.js", "site.compat.min.js"}
V1_FILES = {"widget.module.min.js", "widget.min.js", "widget.polyfilled.min.js"}
class FriendlyPage(HTMLParser):
def __init__(self, page_url):
super().__init__()
self.page_url, self.widget, self.scripts = page_url, {}, []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if "frc-captcha" in (a.get("class") or "").split():
self.widget = a
if tag == "script" and a.get("src") and "nomodule" not in a:
self.scripts.append(urljoin(self.page_url, a["src"]))
def friendly_version(page):
for src in page.scripts:
if "@friendlycaptcha/sdk" in src:
return "v2", src
if "friendly-challenge" in src:
return "v1", src
for src in [s for s in page.scripts if "friendly" in s.lower()]:
name = urlparse(src).path.rsplit("/", 1)[-1]
if name in V2_FILES or name in V1_FILES:
return ("v2" if name in V2_FILES else "v1"), src
keys = page.widget.keys()
if {"data-api-endpoint", "data-form-field-name"} & keys:
return "v2", None
if {"data-puzzle-endpoint", "data-solution-field-name"} & keys:
return "v1", None
raise SystemExit("v1 or v2? Read the page and set it by hand.")
solver = CapSkip(
apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
session = requests.Session()
page = FriendlyPage(PAGE_URL)
page.feed(session.get(PAGE_URL, timeout=30).text)
if not page.widget.get("data-sitekey"):
raise SystemExit("No frc-captcha widget in the HTML; it may be built by JS.")
version, script = friendly_version(page)
try:
result = solver.friendly_captcha(
page.widget["data-sitekey"], PAGE_URL,
version=version, module_script=script,
api_server=page.widget.get("data-api-endpoint")
or page.widget.get("data-puzzle-endpoint"),
)
except ValidationException as exc:
raise SystemExit(f"not sent: {exc}")
except CapSkipError as exc:
raise SystemExit(f"solve failed: {exc!r}")
field = (page.widget.get("data-form-field-name")
or page.widget.get("data-solution-field-name")
or ("frc-captcha-response" if version == "v2" else "frc-captcha-solution"))
# Post wherever the form's action points, with its other fields.
# Browsers send the page as Referer; some servers refuse a post without it.
resp = session.post(PAGE_URL, data={"email": "YOUR_EMAIL", field: result["token"]},
headers={"Referer": PAGE_URL}, timeout=30)
print(resp.status_code, version, field)Versi ditentukan sekali dan dipakai dua kali, untuk pemecahan dan untuk nama field, sehingga keduanya tidak akan pernah bertentangan. Jika parser tidak menemukan widget, biasanya halaman membangunnya dari JavaScript, dan Anda perlu mengambil sitekey dari halaman yang sudah dirender atau dari skrip yang membuatnya. Setiap parameter yang diterima endpoint mentahnya tercantum di referensi API Friendly Captcha.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Situs menolak token yang dikembalikan CapSkip tanpa error | Versi yang dipecahkan salah, sering kali karena v1 menjadi default tanpa peringatan | Tentukan versi di kode seperti di Langkah 2, lalu berikan |
| Ditolak di situs yang widget-nya memiliki data-api-endpoint (v2) atau data-puzzle-endpoint (v1) | Token berasal dari endpoint global, padahal situs memakai endpoint EU | Berikan nilai atribut itu sebagai api_server |
| Token-nya benar, tetapi pengiriman tetap gagal | Token masuk ke field yang salah, form dikirim tanpa Referer, atau situs mengirim JSON | Periksa atribut penggantian nama dan kirim URL halaman sebagai Referer, lalu tiru permintaannya dari DevTools |
| Pemecahan v2 gagal di situs yang meng-host widget-nya sendiri | module_script berupa path relatif, sehingga widget tidak pernah dimuat | Ubah src menjadi alamat lengkap dengan urljoin sebelum memberikannya |
| ApiException berisi ERROR_CAPTCHA_UNSOLVABLE dalam hitungan detik, setiap kali | Friendly Captcha menolak sitekey atau origin halaman, atau akun pemilik sitekey tidak mengaktifkan v2 atau endpoint EU | Periksa sitekey, URL halaman, dan api_server; mencoba ulang tidak akan membantu |
| ValidationException sebelum apa pun terkirim | Sitekey kosong, versi selain v1, v2, 1 atau 2, atau opsi yang tidak dikenal | Pastikan parser menemukan widget, lalu buang opsi yang disebut dalam pesan error |
| Pemecahan makin lambat seiring berjalannya proses yang panjang | Friendly Captcha menaikkan beban kerja untuk alamat yang sibuk | Tambahkan proxy per permintaan atau pool proxy di CapSkip |
| ApiException berisi ERROR_CAPTCHA_UNSOLVABLE setelah dua menit atau lebih | Pemecahan melewati Row Timeout CapSkip sebesar 120 detik, atau menunggu thread kosong melewati Wait Timeout-nya sebesar 250 detik | Samakan semaphore dengan Max. Threads, tambahkan proxy, atau naikkan Row Timeout di pengaturan Friendly Captcha |
| TimeoutException setelah 300 detik | Tugas menunggu thread kosong, lalu dipecahkan lebih lama daripada waktu polling SDK | Samakan semaphore dengan Max. Threads, atau berikan timeout= yang lebih panjang |
| NetworkException pada pemanggilan pertama | CapSkip tidak berjalan, atau host dan port-nya salah | Jalankan CapSkip, lalu periksa apakah seharusnya memakai mode Local atau mode Server |
FAQ
Mengapa tidak mengirim module_script saja dan membiarkan CapSkip yang menentukan?
Bisa, dan untuk skrip yang dimuat dari CDN cara itu berhasil, karena nama paketnya ada di alamat. Risikonya ada pada perilaku fallback. Jika alamat itu tidak menyebut build yang dikenali CapSkip, CapSkip memecahkannya sebagai v1 alih-alih gagal, dan tidak ada apa pun di hasilnya yang memberi tahu Anda. Memeriksa di kode Anda sendiri mengubah default diam-diam itu menjadi exception yang bisa Anda lihat.
Apakah pemecahan ini butuh browser di sisi saya, seperti Selenium atau Playwright?
Tidak. Skrip Anda hanya butuh HTML, yang diambil oleh requests. Semua pekerjaan browser terjadi di dalam CapSkip, yang memecahkan widget v2 di browser miliknya sendiri dan puzzle v1 secara langsung. Jika Anda sudah mengendalikan browser untuk keperluan lain, pemanggilan yang sama tetap berfungsi; cukup baca sitekey dan alamat skrip dari halaman yang sedang terbuka.
Bisakah skrip di VPS atau di cloud memakai pemecah CAPTCHA di PC Windows saya?
Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi agar ia mendengarkan di alamat jaringan, bukan di loopback, setel CAPSKIP_HOST di tempat skrip berjalan, lalu teruskan ke klien seperti pada contoh lengkap. VPS, container, dan hosted runner semuanya terhubung melalui API HTTP yang sama. Gunakan IP publik statis dengan aturan firewall jika rutenya melewati internet. Pemecah CAPTCHA tetap berada di perangkat milik Anda, jadi menjalankan lebih banyak pemecahan tidak pernah mengubah biaya yang Anda bayar.
Apa bedanya dengan panduan C#?
Endpoint mentah, opsi, dan field hasilnya sama di setiap SDK CapSkip; hanya penulisannya yang berubah. Perbedaan sebenarnya ada pada kode di sekitarnya: parser dari pustaka standar, sesi requests yang membawa cookie situs, dan AsyncCapSkip, yang di Python merupakan klien asinkron tersendiri, sedangkan di .NET hanya nama lain untuk CapSkipClient, yang memang sudah asinkron. Versi .NET dari alur kerja ini ada di panduan Friendly Captcha di C#, dan pembahasan lebih lanjut tentang menjalankan pemecahan secara bersamaan di Python ada di panduan pemecahan paralel.
Versi singkatnya
Untuk memecahkan Friendly Captcha di Python, parse halaman untuk menemukan elemen frc-captcha dan tag script-nya, lalu ubah setiap src menjadi alamat lengkap. Tentukan v1 atau v2 dari nama paket, atau dari atribut milik widget itu sendiri, dan munculkan exception jika keduanya tidak memberi jawaban. Panggil friendly_captcha dengan sitekey, URL halaman, versi tersebut, dan alamat skrip, ditambah api_server untuk situs EU. Kirim token sekali saja, di body, pada field yang ditentukan oleh versi dan widget.
- Cara kerja tipe ini dan apa saja yang dicakup pemecah CAPTCHA: halaman pemecah Friendly Captcha.
- Setiap tipe CAPTCHA lain yang dipecahkan paket Python: halaman pemecah CAPTCHA Python.
Satu hal lagi soal volume. Friendly Captcha menagih tingkat kesulitannya dalam waktu CPU, bukan uang, jadi biaya dari hari yang sibuk adalah waktu pemecahan di mesin Anda sendiri. Jalankan pemecah captcha tanpa batas secara lokal, dan satu-satunya batas adalah mesin itu serta thread yang Anda berikan, bukan meteran.
