Cara Memecahkan CAPTCHA di nodriver dengan SDK Async

nodriver captcha - How to Solve CAPTCHA in nodriver With the Async SDK

Langkah captcha di nodriver terdiri dari tiga gerakan yang biasa: baca sitekey dari halaman, kirim ke solver, tulis token itu kembali dengan JavaScript. Yang berbeda adalah nodriver bersifat asinkron sampai ke lapisan terdalam. Ia mengendalikan Chrome lewat websocket asyncio, jadi proses pemecahan yang memblokir bukan cuma membuat skrip Anda menunggu, tetapi juga menghentikan socket yang membawa setiap pesan DevTools. Pasangkan dengan client async dan semuanya tetap responsif.

Apa yang Anda butuhkan

  • Python 3.10 atau lebih baru, dengan nodriver 0.50 dan SDK CapSkip terinstal.
  • Chrome, Chromium, Edge, atau Brave terinstal di tempat skrip dijalankan. nodriver meluncurkannya secara langsung.
  • URL halaman dari formulir yang dilindungi. Sitekey dibaca saat runtime.
  • CapSkip yang berjalan dalam mode Local ketika skrip dan solver berada di satu mesin, atau dalam mode Server ketika keduanya tidak berada di satu mesin. Keduanya dijelaskan di pengaturan koneksi.
# Both packages, one line.
pip install nodriver capskip

Mengapa nodriver mengubah bentuk pekerjaan ini

nodriver adalah penerus resmi undetected-chromedriver, ditulis oleh penulis yang sama, dan poin utamanya adalah tidak ada webdriver dan tidak ada Selenium di mana pun dalam stack-nya. Ia berbicara dengan browser yang diluncurkannya sendiri lewat DevTools Protocol. Tidak ada binary chromedriver yang perlu ditambal, tidak ada versi driver yang harus disamakan dengan Chrome.

Bagian yang penting untuk pekerjaan CAPTCHA ada di paruh kedua kalimat itu: nodriver sepenuhnya asinkron. Koneksinya berupa websocket yang ditangani asyncio, dan sebuah task latar belakang membaca pesan protokol dari sana. Setiap pencarian elemen, setiap navigasi, dan setiap event handler bergantung pada task itu mendapat jatah jadwal. Panggil solver sinkron di tengah-tengahnya dan tidak ada lagi yang berjalan di dalam proses tersebut selama proses pemecahan berlangsung, yang untuk reCAPTCHA v2 lazimnya lima belas sampai empat puluh lima detik.

Jadi aturan untuk framework ini singkat saja. Gunakan client async, dan await panggilannya.

Satu hal yang perlu ditegaskan: tanpa webdriver bukan berarti tanpa deteksi. Menghapus driver menghapus satu sinyal dan meninggalkan sisa fingerprint Anda persis di tempatnya semula. Memecahkan challenge dan membuat diri Anda terlihat seperti browser biasa adalah dua pekerjaan yang terpisah, dan artikel ini membahas yang pertama.

Langkah 1: baca sitekey dari halaman

Sitekey berada di dokumen induk, bukan di dalam iframe widget. Markup milik Google menaruhnya pada sebuah container sebagai atribut data-sitekey, dan metode select milik nodriver menemukan container itu lewat selector CSS. Perhatikan akses bracket di baris terakhir, karena hal itulah yang paling sering menjebak orang.

# pip install nodriver
import nodriver as uc

async def main():
    browser = await uc.start()
    page = await browser.get("https://example.com/page-with-recaptcha")

    # select() retries for 10 seconds by default, so it doubles
    # as a wait condition for a widget that renders late.
    holder = await page.select("div.g-recaptcha")
    sitekey = holder.attrs["data-sitekey"]

    print(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

uc.loop().run_until_complete(main())

Nama atribut disimpan pada elemen persis seperti yang dituliskan HTML, termasuk tanda hubungnya, jadi akses bracket adalah satu-satunya cara yang bisa diandalkan untuk membacanya. Jalan pintas dengan titik terlihat seolah seharusnya bekerja padahal diam-diam tidak: meminta data_sitekey pada sebuah elemen justru mengembalikan None dan bukan melempar error, karena pencarian itu jatuh ke sebuah default. None tersebut lalu berjalan sampai ke solver sebagai key kosong dan baru muncul jauh kemudian sebagai ERROR_GOOGLEKEY, jauh dari baris yang menyebabkannya. Hanya ada satu penggantian nama yang benar-benar perlu Anda ketahui, yaitu bahwa atribut class disimpan di bawah class_ agar tidak bentrok dengan kata kunci Python.

Sebagian situs tidak pernah menampilkan sitekey di halaman induk dan hanya mengopernya lewat URL iframe widget. Baca saja dari query string-nya.

# Fallback: the k= parameter on the anchor iframe.
from urllib.parse import urlparse, parse_qs

frame = await page.select("iframe[src*='recaptcha/api2/anchor']")
sitekey = parse_qs(urlparse(frame.attrs["src"]).query)["k"][0]

Langkah 2: pecahkan tanpa menghentikan socket

SDK Python menyediakan dua client. CapSkip bersifat sinkron dan AsyncCapSkip adalah implementasi asyncio yang sesungguhnya, bukan sekadar alias, dan itulah yang persis dibutuhkan framework ini. Keduanya berbicara dengan solver di mesin Anda sendiri pada port 8080, dan tidak satu pun menagih per pemecahan.

# pip install capskip
from capskip import AsyncCapSkip

solver = AsyncCapSkip(host="127.0.0.1", port=8080)

# Same call shape for v3 (version="v3") and Enterprise
# (enterprise=1). Invisible v2 takes invisible=1.
result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)

token = result["code"]   # the g-recaptcha-response value

Karena panggilannya awaitable, beberapa tab bisa memecahkan CAPTCHA sekaligus tanpa threading sama sekali. Buka halamannya, kumpulkan hasil pemecahannya, lalu suntikkan setiap token ke tab yang sesuai. Pola yang lebih luas, termasuk bagaimana SDK memundurkan interval polling-nya alih-alih tidur pada interval tetap, dibahas di memecahkan CAPTCHA secara paralel.

# Three tabs, three solves, one wait.
import asyncio

results = await asyncio.gather(*[
    solver.recaptcha(sitekey=k, url=u) for k, u in targets
])

Turnstile dan GeeTest punya metode sendiri, dan keduanya berbentuk sama dengan panggilan di atas. Daftar parameter lengkap untuk masing-masing ada di dokumentasi API CapSkip.

Langkah 3: suntikkan token dan kirim

Textarea respons disembunyikan dengan display:none, jadi mengetik ke dalamnya bukan pilihan di alat otomatisasi mana pun. Anda menuliskannya dengan JavaScript. Metode evaluate milik nodriver menerima sebuah string ekspresi tanpa ada cara untuk mengoper argumen bersamanya, jadi token harus ditanamkan di dalam string itu, dan cara aman untuk melakukannya adalah json.dumps, bukan f-string. Literal string JSON adalah literal string JavaScript yang valid, lengkap dengan tanda kutip dan escape-nya.

# json.dumps gives a correctly quoted JS string literal.
import json

await page.evaluate(
    "document.getElementById('g-recaptcha-response').value = "
    + json.dumps(token)
)

# Then submit the form the way the page expects.
button = await page.select("button[type=submit]")
await button.click()

Memeriksa bahwa nilainya benar-benar masuk punya jebakannya sendiri, dan itu layak sepuluh detik perhatian Anda. Dengan return_by_value diaktifkan, evaluate hanya mengembalikan nilai Python biasa ketika nilai itu truthy. String kosong atau angka nol akan lolos dan Anda malah mendapat objek protokol. Jadi jangan membaca panjangnya lalu mengujinya, karena panjang nol justru kasus yang sedang ingin Anda deteksi. Kembalikan sesuatu yang tidak mungkin bernilai falsy.

# String() keeps a zero-length answer truthy, so the check
# reports the real number instead of a protocol object.
length = await page.evaluate(
    "String(document.getElementById('g-recaptcha-response').value.length)",
    return_by_value=True,
)
print(length)   # "0" means the injection did not land

Jika situs mendefinisikan callback alih-alih membaca textarea saat submit, panggil callback itu setelah Anda menetapkan nilainya. Nama fungsinya berbeda-beda per situs, jadi baca dari markup halaman itu sendiri daripada menebak-nebak. Ini tetap reCAPTCHA v2 biasa dalam kedua kasus: callback mengubah cara Anda menyerahkan token, bukan cara token itu dipecahkan. Halaman pemecah reCAPTCHA v2 membahas kedua gaya pengiriman tersebut.

Contoh lengkap yang berfungsi

Semua yang di atas dalam satu skrip. Solver dibuat sekali lalu dipakai ulang, dan browser dihentikan di dalam blok finally agar proses pemecahan yang gagal tidak meninggalkan proses Chrome yang menggantung.

# pip install nodriver capskip
import json
import nodriver as uc
from capskip import AsyncCapSkip

PAGE_URL = "https://example.com/page-with-recaptcha"

async def main():
    solver = AsyncCapSkip(host="127.0.0.1", port=8080)
    browser = await uc.start()
    try:
        page = await browser.get(PAGE_URL)

        holder = await page.select("div.g-recaptcha")
        sitekey = holder.attrs["data-sitekey"]
        if not sitekey:
            raise RuntimeError("Widget found but data-sitekey was empty.")

        result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)
        await page.evaluate(
            "document.getElementById('g-recaptcha-response').value = "
            + json.dumps(result["code"])
        )

        button = await page.select("button[type=submit]")
        await button.click()
        await page.sleep(2)

        print(page.target.url)   # the page you land on after submitting
    finally:
        browser.stop()

uc.loop().run_until_complete(main())

Menjalankan solver di mesin lain

Cepat atau lambat nodriver berakhir di sebuah server, dan di sana ia punya dua kebutuhan: binary Chromium, dan sesi desktop untuk tempat Chrome menggambar. Mode headless mati secara default, jadi server yang tidak punya sesi desktop butuh headless dinyalakan secara eksplisit. Solver tidak harus ikut pindah bersamanya.

CapSkip punya dua mode koneksi. Local mengikat ke 127.0.0.1 dan hanya melayani perangkat itu, dan itulah pengaturan yang tepat selama Anda masih menulis skripnya. Server mengikat ke alamat jaringan atau IP publik Anda, sehingga VM scraping, host container, atau workstation kedua bisa memanggil solver yang sama lewat API. IP publik statis menjaga alamat itu tetap stabil. Tidak ada yang berubah di dalam kode selain host yang Anda oper, dan tidak ada pula yang berubah soal biayanya, karena ini tetap perangkat keras Anda sendiri.

# Same SDK, same call. Only the host moves.
solver = AsyncCapSkip(host="10.0.0.12", port=8080, apiKey="YOUR_API_KEY")

Nyalakan validasi key begitu solver mendengarkan di alamat jaringan, dan beri setiap mesin key-nya sendiri agar satu key bisa dicabut tanpa mengusik yang lain. Lihat panduan penyiapan yang membahas kedua mode tersebut.

Ada satu tabrakan penamaan yang perlu Anda jaga sambil melakukannya. Fungsi start milik nodriver sendiri juga menerima host dan port, dan keduanya menggambarkan endpoint debugging Chrome yang ingin Anda lekati, bukan solver. Berikan keduanya dan nodriver justru tidak akan meluncurkan browser sama sekali. Alamat solver adalah milik konstruktor client dan tidak di tempat lain.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
AttributeError saat pencarian di attrsselect() tidak menemukan apa pun dan mengembalikan NonePerlebar selector, atau naikkan timeout select
Sitekey bernilai None tanpa error sama sekaliAkses dengan titik tidak bisa menjangkau atribut bertanda hubungBaca dari attrs dengan akses bracket
ERROR_GOOGLEKEYSitekey kosong sampai ke solverPeriksa nilainya sebelum menghabiskan satu proses pemecahan untuknya
Skrip menggantung selama seluruh proses pemecahanClient sinkron memblokir event loopGunakan AsyncCapSkip dan await panggilannya
NetworkExceptionCapSkip tidak berjalan, atau host-nya salahJalankan aplikasinya, atau arahkan host ke alamat server
TimeoutExceptionProses pemecahan melampaui recaptchaTimeoutNaikkan di atas nilai default 300 detik
evaluate mengembalikan objek, bukan stringNilainya falsy, jadi pengembalian nilai biasa dilewatiBungkus panjangnya dengan String(), bukan nilainya

FAQ

Bisakah saya tetap memakai client sinkron kalau saya hanya memecahkan sekali?

Bisa, dan pada skrip pendek mungkin Anda tidak akan pernah merasakannya. Yang Anda korbankan adalah setiap pesan protokol yang datang selama proses pemecahan: event navigasi, event load, apa pun yang sedang ditunggu sebuah event handler. Pada run yang panjang hal itu muncul sebagai pencarian elemen yang kena timeout tanpa alasan yang terlihat. Client async hanya berbiaya satu import dan satu await, jadi tidak banyak alasan untuk mengambil pertukaran itu.

Apakah saya perlu masuk ke dalam iframe reCAPTCHA?

Tidak, dan justru bagian inilah yang sering dibuat terlalu rumit orang. Checkbox-nya memang ada di dalam iframe, tetapi atribut sitekey dan textarea respons yang tersembunyi sama-sama milik dokumen induk. Anda baru menyentuh frame ketika situs tidak menaruh sitekey di halaman dan Anda harus membacanya dari URL frame itu sendiri.

Saya sedang bermigrasi dari undetected-chromedriver. Apa saja yang terbawa?

Ketiga gerakannya terbawa tanpa perubahan, karena semuanya memang tidak pernah terikat pada driver: baca sitekey, pecahkan, tulis token ke dalam textarea. Yang tidak terbawa adalah API di sekelilingnya, karena kini setiap panggilan bersifat awaitable dan tidak ada lagi objek driver. nodriver juga menyertakan helper yang mengubah instance undetected-chromedriver yang sedang berjalan menjadi objek browser, sehingga Anda bisa memindahkan skrip secara bertahap. Pendekatan lama dibahas di panduan CAPTCHA undetected-chromedriver.

Scraper saya berjalan di VPS. Solver-nya ditaruh di mana?

Di mana pun Anda mau, asal keduanya bisa saling menjangkau. Mode Server membuat solver mendengarkan di alamat jaringan alih-alih alamat loopback, sehingga VPS memanggilnya lewat API persis seperti layanan internal lainnya. Arahkan argumen host ke alamat itu, aktifkan validasi key, dan beri VPS key-nya sendiri. Solver sendiri tidak butuh display, dan itu menguntungkan mengingat browser-nya justru butuh.

Versi singkatnya

Baca sitekey dengan akses bracket pada attrs, pecahkan dengan AsyncCapSkip di 127.0.0.1:8080, suntikkan token lewat evaluate dengan json.dumps, lalu kirim. Await semuanya, karena proses pemecahan yang sinkron menahan socket yang mengendalikan browser. Untuk gambaran Python yang lebih luas, termasuk Selenium dan Playwright, lihat halaman pemecah CAPTCHA Python.

Satu konsekuensi layak dijabarkan sebelum Anda menaikkan skala crawl. Karena CapSkip adalah pemecah captcha tanpa batas yang berjalan di perangkat keras Anda sendiri, run yang mengulang seribu halaman berbiaya persis sama dengan run yang mengulang sepuluh halaman.