Cara Memecahkan ALTCHA di Scrapy dengan Satu Permintaan Inline

solve altcha in scrapy - How to Solve ALTCHA in Scrapy With One Inline Request

Untuk memecahkan ALTCHA di Scrapy, baca alamat challenge dari elemen altcha-widget, ambil dengan permintaan inline dari callback yang sama, serahkan JSON-nya ke metode altcha milik AsyncCapSkip, lalu kirim form dengan token di field yang dinamai widget, yang secara default bernama altcha. ALTCHA adalah proof of work, bukan gambar, jadi tidak ada satu pun dari langkah ini yang membutuhkan browser, dan spider Scrapy biasa sudah cukup. Satu-satunya jebakan khas Scrapy mudah terlewat: endpoint challenge mengembalikan dokumen baru di URL yang sama setiap kali, dan filter duplikat Scrapy diam-diam membuang permintaan kedua ke endpoint itu. Panduan ini membahas widget, pengambilan, pemecahan dan pengiriman, lengkap dengan spider yang bisa Anda jalankan.

Apa yang Anda butuhkan

  • Scrapy 2.14 atau lebih baru, untuk metode permintaan inline yang dipakai di bawah. Sejak 2.13, Scrapy secara default berjalan di reactor asyncio, dan itulah yang memungkinkan callback melakukan await pada klien async. Salah satu bagian menjelang akhir membahas versi yang lebih lama.
  • Paket Python capskip versi 1.2.0 atau lebih baru, yaitu rilis yang menambahkan metode altcha, dan CapSkip 1.3.0 atau lebih baru yang berjalan di komputer Windows.
  • Paket form2request, yang kini direkomendasikan Scrapy untuk menyusun pengiriman form.
  • Halaman yang berisi form. Semua hal lainnya, termasuk challenge, diambil dari 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 crawler di mesin lain dapat memanggilnya melalui API. Keduanya ada di pengaturan koneksi, dan salah satu bagian di bawah membahas kapan Anda perlu beralih.
# pip install scrapy capskip form2request
pip install scrapy capskip form2request

Langkah 1: baca challenge dari widget

ALTCHA tidak punya sitekey. Yang Anda butuhkan untuk memecahkan ALTCHA di Scrapy adalah challenge-nya, atau alamat asalnya, dan elemen widget membawa salah satunya. Atribut mana yang memuatnya bergantung pada generasi widget, jadi baca ketiganya.

WidgetAtributIsinya
v1 dan v2challengeurl, atau challengejsonchallengeurl menyebut endpoint yang menyajikan challenge, sering kali berupa path relatif; challengejson membawa dokumen challenge itu sendiri
v3 dan setelahnyachallengeEndpoint tersebut, atau dokumen challenge itu sendiri
# Inside a spider callback; response is the page with the form.
widget = response.css("altcha-widget")
source = (widget.attrib.get("challenge")
          or widget.attrib.get("challengejson")
          or widget.attrib.get("challengeurl"))

# The token goes in a field named by the widget, altcha by default.
field = widget.attrib.get("name", "altcha")

# A v3 challenge or a v1/v2 challengejson carries the document inline.
inline = source.lstrip().startswith("{")

Perhatikan juga atribut name. Widget menulis payload-nya ke hidden input dengan nama tersebut, yang secara default adalah altcha, tetapi situs bisa mengubahnya. Membacanya hanya butuh satu baris dan mencegah pengiriman yang diam-diam membiarkan field sebenarnya kosong. Beberapa penerapan membuat challenge di script milik halaman itu sendiri alih-alih menyajikannya dari endpoint, dan dalam kasus itu tidak ada atribut yang bisa dibaca. Tab Network menunjukkan jenis mana yang Anda hadapi.

Langkah 2: ambil challenge di dalam callback

Jika widget menunjuk ke sebuah endpoint, ambil endpoint itu dari callback tempat Anda sedang berada, dengan metode download_async milik engine, yang dijelaskan dokumentasi Scrapy di bagian permintaan inline. Permintaan itu melalui downloader middleware, jadi cookie jar, user agent dan pengaturan proxy milik spider berlaku padanya, sama seperti pada halaman. Permintaan itu tidak melalui scheduler, dan justru itulah intinya.

# The page's jar and proxy; no scheduler, no dupefilter.
meta = {"dont_cache": True, "allow_offsite": True}
for key in ("cookiejar", "proxy"):
    if key in response.meta:
        meta[key] = response.meta[key]
reply = await self.crawler.engine.download_async(
    scrapy.Request(response.urljoin(source), meta=meta,
                   headers={"Referer": response.url})
)
if reply.status != 200:
    raise ValueError(f"challenge endpoint answered {reply.status}")
challenge_json = reply.text

Inilah alasan mengapa melompati scheduler itu penting. Alternatif yang paling lazim, yaitu melakukan yield Request untuk challenge dengan callback tersendiri, berhasil untuk form pertama dan kehilangan form kedua. Setiap pengambilan dari endpoint mengembalikan challenge baru, tetapi URL-nya tidak pernah berubah, sehingga filter duplikat Scrapy menganggap permintaan kedua sebagai permintaan yang sudah pernah dibuat dan membuangnya. Tidak ada error. Filter itu mencatat pembuangan pertamanya sekali di level debug, menaikkan sebuah penghitung di statistik crawl, dan form di baliknya begitu saja tidak pernah dikirim. Permintaan inline tidak pernah sampai ke filter.

Sisa blok itu menangani detail yang biasanya diurus scheduler untuk Anda. State per permintaan tidak terbawa dengan sendirinya, jadi loop menyalin key cookiejar atau proxy dari meta halaman ketika halaman itu diambil dengan salah satunya. Header Referer disetel secara manual karena middleware yang menambahkannya berjalan di sisi spider. Jika endpoint challenge berada di domain lain, misalnya layanan ALTCHA yang di-hosting, dan spider menyetel allowed_domains, filter offsite akan memblokir pengambilan itu, dan allow_offsite meloloskan satu permintaan ini. Pemeriksaan status ada karena 403 dari endpoint kembali sebagai respons biasa di sini: middleware yang mengubah status error menjadi kegagalan berjalan di sisi spider, yang dilompati oleh permintaan inline. Lalu dont_cache menjauhkan HTTP cache Scrapy dari challenge jika Anda mengaktifkannya. Challenge yang di-cache adalah challenge yang sudah kedaluwarsa, dan CapSkip langsung menolak challenge inline yang kedaluwarsa alih-alih menghabiskan CPU untuk token yang akan ditolak situs. Tanpa flag itu, proses kedua memberikan challenge tersimpan yang sama ke setiap halaman yang berbagi endpoint tersebut. Logika yang sama berlaku untuk halaman form. Halaman yang di-cache memutar ulang token anti-forgery dan cookie sesi yang lama, dan jika challenge-nya inline, juga challenge yang lama, jadi dengan cache aktif, pasang dont_cache pada halaman form juga, atau matikan cache untuk spider ini.

Sebagai gantinya, Anda bisa memberikan endpoint sebagai challenge_url dan membiarkan CapSkip mengambil challenge sendiri. Itu tidak masalah untuk endpoint publik. Namun pengambilan oleh CapSkip tidak membawa satu pun cookie spider Anda, dan beberapa situs hanya menerbitkan challenge ke sesi yang memuat form. Mengambil lewat Scrapy menjaga satu sesi dari awal hingga akhir.

Langkah 3: pecahkan tanpa memblokir crawl

Gunakan AsyncCapSkip dan jadikan callback sebagai async def. Di paket Python, class itu adalah klien asyncio sungguhan, bukan sekadar alias, jadi melakukan await padanya mengembalikan kendali ke Scrapy selama pemecahan berjalan dan setiap permintaan lain terus bergerak. Class CapSkip yang sinkron akan menghentikan seluruh crawl selama setiap pemecahan berlangsung, masalah yang dijelaskan secara rinci di panduan middleware CAPTCHA Scrapy.

from capskip import AsyncCapSkip

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

# Pass the document exactly as the endpoint sent it.
result = await solver.altcha(url=response.url, challenge_json=challenge_json)

token = result["token"]   # base64 payload for the form field

Karena challenge dikirim secara inline, CapSkip sama sekali tidak membuat permintaan jaringan. Ia melakukan hashing sampai menemukan jawabannya, yang biasanya hanya butuh beberapa milidetik, dan klien langsung melakukan polling lalu sekali lagi seperempat detik kemudian, sehingga pemecahan biasa kembali dalam sekitar seperempat detik. Pemanggilan ini menunggu hingga defaultTimeout, 120 detik, karena ini pekerjaan CPU, bukan sesi browser. Dua algoritma challenge, Argon2id dan scrypt, ditolak alih-alih dicoba, dan penolakan itu datang sebagai ApiException dalam waktu kurang dari satu detik. Skema PBKDF2 dan SHA yang lebih umum, termasuk default yang direkomendasikan ALTCHA, sudah dicakup.

Berikan teks balasan apa adanya seperti saat diterima; tidak perlu mem-parse-nya. Yang harus tetap utuh adalah token: token adalah base64 dari dokumen JSON yang field-nya sudah ditandatangani server situs, jadi token yang dipotong, di-decode, atau di-encode ulang akan gagal verifikasi.

Langkah 4: kirim form bersama token

Hidden input yang diisi widget tidak ada di HTML yang diunduh Scrapy. Widget membuatnya di browser, setelah widget berjalan, jadi apa pun yang membaca HTML yang disajikan tidak akan menemukannya. Tambahkan sendiri, dengan nama dari Langkah 1. Contoh ini menyusun pengiriman dengan form2request, yang direkomendasikan Scrapy sebagai pengganti FormRequest.from_response: sejak Scrapy 2.16, metode lama itu mencatat peringatan deprecation pada setiap pemanggilan.

from form2request import form2request

form = response.xpath("//form[.//altcha-widget]")
data = {"email": "[email protected]", field: token}
yield form2request(form, data).to_scrapy(
    callback=self.after_submit,
    priority=10,
    meta={"handle_httpstatus_all": True},
)

form2request mempertahankan apa yang sudah dimasukkan halaman ke dalam form, seperti token anti-forgery, dan menekan tombol submit pertama, sehingga permintaannya terlihat seperti yang akan dikirim browser. XPath memilih form yang berisi widget, dan itu penting pada halaman yang juga memuat kotak pencarian. handle_httpstatus_all memungkinkan callback melihat pengiriman yang ditolak, yang jika tidak akan dibuang Scrapy sebelum sampai. Kenaikan priority menempatkan pengiriman di depan semua yang masih menunggu di scheduler, karena challenge bisa kedaluwarsa. Beberapa jendela waktu hanya berlangsung sekitar dua menit, dan token yang menunggu di belakang crawl yang panjang bisa kedaluwarsa sebelum terkirim.

Anggap setiap token hanya berlaku untuk satu pengiriman. Jika situs menolak form, mulai lagi dari Langkah 2 dengan challenge baru alih-alih mencoba ulang permintaan yang sama. Sebagian integrasi mengirim payload di body JSON atau cookie, bukan di field form, jadi kirim form sekali secara manual dengan DevTools terbuka lalu tiru apa yang dikirim halaman itu.

Menjalankan pemecah di tempat lain

Contoh kode memakai 127.0.0.1 karena itu benar selama spider dan CapSkip berada di mesin yang sama. Crawler yang di-deploy ke Scrapyd di mesin lain, ke VPS, atau ke platform Scrapy yang di-hosting mengarahkan loopback ke dirinya sendiri, dan pemecahan pertama memicu NetworkException. Alihkan CapSkip ke mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda, sehingga spider dapat menjangkaunya melalui API dari mana pun yang Anda izinkan. Gunakan IP publik statis jika rutenya melewati internet, aktifkan validasi API key, dan batasi port hanya untuk alamat yang Anda harapkan dengan aturan Windows Firewall. Mesinnya tetap mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.

Klien tidak membaca variabel lingkungan dengan sendirinya. Baca CAPSKIP_HOST dan CAPSKIP_API_KEY di spider dan berikan ke konstruktor, seperti pada contoh lengkap, agar kode yang sama bisa berjalan di komputer Anda maupun di server.

Contoh lengkap yang berfungsi

# pip install scrapy capskip form2request
import os

import scrapy
from capskip import AsyncCapSkip, CapSkipError
from form2request import form2request


class SignupSpider(scrapy.Spider):
    name = "signup"
    start_urls = ["https://example.com/signup"]

    solver = AsyncCapSkip(
        apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
        host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
        port=8080,
    )

    async def parse(self, response):
        widget = response.css("altcha-widget")
        source = (widget.attrib.get("challenge")
                  or widget.attrib.get("challengejson")
                  or widget.attrib.get("challengeurl"))
        if not source:
            self.logger.warning("no ALTCHA challenge on %s", response.url)
            return
        field = widget.attrib.get("name", "altcha")

        if source.lstrip().startswith("{"):
            challenge_json = source
        else:
            # Inline request: the page's jar and proxy, no dupefilter.
            meta = {"dont_cache": True, "allow_offsite": True}
            for key in ("cookiejar", "proxy"):
                if key in response.meta:
                    meta[key] = response.meta[key]
            reply = await self.crawler.engine.download_async(
                scrapy.Request(response.urljoin(source), meta=meta,
                               headers={"Referer": response.url})
            )
            if reply.status != 200:
                self.logger.error("challenge endpoint answered %s", reply.status)
                return
            challenge_json = reply.text

        try:
            result = await self.solver.altcha(
                url=response.url, challenge_json=challenge_json)
        except CapSkipError as exc:
            # Expired challenge, unsupported algorithm, or CapSkip unreachable.
            self.logger.error("ALTCHA not solved on %s: %r", response.url, exc)
            return

        form = response.xpath("//form[.//altcha-widget]")
        data = {"email": "[email protected]", field: result["token"]}
        yield form2request(form, data).to_scrapy(
            callback=self.after_submit,
            priority=10,
            meta={"handle_httpstatus_all": True},
        )

    def after_submit(self, response):
        yield {"url": response.url, "status": response.status}

Jalankan dengan scrapy runspider dan nama file-nya, atau masukkan class ini ke dalam sebuah proyek. Spider ini menangani kedua generasi widget, mengambil challenge di dalam sesi halaman, dan melakukan yield satu item per form yang dikirim, lengkap dengan status jawaban situs, termasuk penolakan. Karena pengambilan challenge tidak melalui scheduler, setiap halaman yang berbagi satu endpoint challenge mendapat challenge-nya sendiri. Endpoint mentah di balik metode altcha didokumentasikan di referensi API, dan pemanggilan Python biasa tanpa Scrapy di sekitarnya dibahas di panduan ALTCHA untuk Python.

Pada Scrapy yang lebih lama dari 2.14

Sebelum 2.14 tidak ada download_async, tetapi sejak Scrapy 2.6 pengambilan inline yang sama bisa dilakukan lewat engine.download, yang di-await dengan helper yang mengubah hasilnya menjadi sesuatu yang bisa ditunggu oleh coroutine.

from scrapy.utils.defer import maybe_deferred_to_future

# Scrapy 2.6 to 2.13: the same inline fetch, through engine.download.
reply = await maybe_deferred_to_future(self.crawler.engine.download(
    scrapy.Request(response.urljoin(source),
                   meta={"dont_cache": True, "allow_offsite": True})
))

Pada versi tersebut, FormRequest.from_response menyusun pengiriman tanpa peringatan, jadi Anda bisa memakainya sebagai pengganti form2request: berikan XPath yang sama sebagai formxpath dan masukkan token ke formdata. Jika Anda justru mengarahkan challenge melalui scheduler, sebagai permintaan biasa dengan callback tersendiri, setel dont_filter padanya, atau filter duplikat akan membuang setiap pengambilan setelah yang pertama.

Pada Scrapy yang lebih lama dari 2.13, AsyncCapSkip juga membutuhkan TWISTED_REACTOR yang disetel ke reactor asyncio di settings.py. Proyek yang dibuat sejak Scrapy 2.7 sudah memiliki baris tersebut.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
Form pertama terkirim dan form berikutnya tidak pernah terkirim, tanpa errorChallenge di-yield sebagai permintaan biasa dan filter duplikat membuang pengulangannyaAmbil dengan download_async, atau setel dont_filter pada permintaan itu
ApiException dalam sekejap, tetapi hanya setelah proses pertamaHTTP cache memutar ulang challenge lama yang sudah kedaluwarsaTambahkan dont_cache ke permintaan challenge
Situs menyatakan verifikasi gagal padahal pemecahan berhasilToken menunggu terlalu lama, dipakai ulang, atau masuk ke field yang salahKirim segera dengan priority yang dinaikkan, sekali per token, dengan nama dari atribut name milik widget
403 dari endpoint challengeEndpoint menginginkan sesi yang memuat formAmbil lewat Scrapy, seperti di atas, bukan lewat challenge_url
ApiException dalam waktu kurang dari satu detik, setiap kaliSitus memakai Argon2id atau scrypt, yang ditolak alih-alih dicobaTidak ada yang perlu dicoba ulang; situs itu membutuhkan jalur lain
Crawl macet selama setiap form dipecahkanClass CapSkip yang sinkron berjalan di event loopBeralih ke AsyncCapSkip dan callback async def
NoEventLoopError, not currently running on any asynchronous event loop (AsyncLibraryNotFoundError pada instalasi lama)Proyek mengunci reactor selain reactor asyncioHapus baris TWISTED_REACTOR itu, atau setel ke reactor asyncio
AttributeError: ‘ExecutionEngine’ object has no attribute ‘download_async’Versi Scrapy lebih lama dari 2.14Perbarui Scrapy, atau pakai engine.download seperti yang ditunjukkan di atas
IgnoreRequest, filtered offsite request, pada pengambilan challengeEndpoint challenge berada di domain lain dan spider menyetel allowed_domainsTambahkan allow_offsite ke meta permintaan challenge
ScrapyDeprecationWarning tentang from_response pada setiap pengirimanScrapy 2.16 dan yang lebih baru menandai FormRequest.from_response sebagai deprecatedSusun pengiriman dengan form2request
NetworkException pada pemecahan pertamaCapSkip tidak berjalan, atau host dan port-nya salahJalankan CapSkip, lalu periksa apakah ia harus berada di mode Local atau mode Server

FAQ

Apakah saya butuh scrapy-playwright atau headless browser untuk ALTCHA?

Tidak, Anda bisa memecahkan ALTCHA di Scrapy dengan permintaan biasa. Seluruh tugas widget hanyalah mengambil challenge, menghabiskan sedikit CPU untuk mencari sebuah angka, dan menulis hasilnya ke sebuah field. Scrapy bisa mengambil challenge, CapSkip menemukan angkanya, dan form2request menulis field-nya, jadi tidak ada JavaScript yang perlu dijalankan di mana pun. Dengan begitu, spider tetap secepat dan semurah crawl Scrapy lainnya untuk dijalankan.

Bisakah spider di Scrapyd atau platform Scrapy yang di-hosting menjangkau pemecah CAPTCHA?

Bisa. Alihkan CapSkip ke mode Server di pengaturan koneksi agar ia mendengarkan di alamat jaringan, bukan di loopback, setel CAPSKIP_HOST di lingkungan spider, lalu berikan ke klien seperti pada contoh lengkap. Platform yang di-hosting terhubung melalui API HTTP yang sama dengan spider lokal. Gunakan IP publik statis dengan aturan firewall ketika rutenya melewati internet. Pemecah CAPTCHA tetap berada di perangkat milik Anda, jadi jumlah pemecahan tidak pernah mengubah apa yang Anda bayar.

Mengapa ini ada di callback dan bukan di downloader middleware?

Karena ALTCHA berada pada form yang Anda pilih untuk dikirim, bukan pada halaman blokir yang menyela crawl. Middleware adalah tempat yang tepat untuk challenge yang bisa muncul di respons mana pun, dan begitulah cara panduan middleware reCAPTCHA menanganinya. ALTCHA adalah bagian dari satu langkah dalam alur, dan callback yang menyusun form itu sudah memiliki semua yang dibutuhkan pemecahan.

Bisakah saya memecahkan banyak form secara paralel?

Bisa, dan Scrapy sudah melakukannya untuk Anda. Setiap callback async yang melakukan await pada pemecah CAPTCHA menyerahkan kendali, sehingga setiap halaman yang sudah diunduh Scrapy bisa menjalankan pemecahannya pada saat yang sama. CONCURRENT_REQUESTS tidak membatasi hal itu, karena yang dibatasinya adalah unduhan, bukan callback. Di sisi CapSkip, Max. Threads di pengaturan ALTCHA menentukan berapa banyak challenge yang di-hash sekaligus, dan karena itu pekerjaan CPU, thread yang lebih banyak daripada jumlah core tidak memberi keuntungan apa pun. Karena setiap callback mengambil challenge-nya sendiri tepat sebelum memecahkan, tidak ada satu pun yang menunggu di antrean hingga basi. Untuk alasan yang sama, jangan mengambil sekumpulan challenge lebih dulu untuk dipecahkan belakangan.

Versi singkatnya

Untuk memecahkan ALTCHA di Scrapy, baca atribut challenge, challengejson atau challengeurl serta atribut name dari elemen altcha-widget. Ambil challenge dengan download_async dan dont_cache, agar ia menumpang sesi spider dan tidak pernah bertemu filter duplikat. Lakukan await pada metode altcha milik AsyncCapSkip dengan JSON apa adanya seperti saat diterima, lalu yield pengiriman form2request dengan token di data-nya dan priority yang dinaikkan. Satu challenge, satu token, satu pengiriman, dan mode Server ketika spider berjalan di tempat lain.

Satu hal terakhir tentang volume. Crawler yang mengirim ribuan form memecahkan ribuan challenge, dan dengan pemecah captcha yang berjalan di mesin Anda sendiri, itu hanya memakan waktu CPU, bukan biaya per pemecahan.