Scarnox · Blog

Middleware captcha untuk Scrapy: pecahkan captcha di spider Anda (2026)

Scrapy dibangun di sekitar request dan callback, jadi captcha tidak cocok dengan rapi di dalamnya. Cara yang bersih untuk menanganinya adalah middleware downloader: ia melihat setiap respons, mendeteksi tantangan, mendapatkan token, dan membiarkan spider mengirim form dengan token tersebut. Panduan ini membangun middleware itu untuk halaman PopularCaptcha, yang saat ini dipecahkan Scarnox secara proxyless.

Mulai dengan kredit uji coba gratis, tanpa kartu dan coba kodenya pada halaman uji sebelum Anda menjalankannya pada hal lain.

Cara kerja middleware

Middleware berjalan pada setiap respons:

  1. Jika respons tidak memiliki data-sitekey, middleware meneruskan respons tersebut.
  2. Jika ada, middleware memecahkan captcha dan mengembalikan salinan request yang sama dengan token disimpan di meta.
  3. Scrapy mengambil ulang request itu. Middleware melihat token di meta dan membiarkan respons lewat.
  4. Spider membaca token dari meta dan mengirim form dengan FormRequest.from_response.

Panggilan solve berjalan di thread melalui deferToThread, sehingga tidak memblokir reactor Scrapy. Request lain tetap berjalan sementara captcha sedang dipecahkan.

Pengaturan proyek

Simpan kunci Anda di environment variable, bukan di kode:

# settings.py
import os

SCARNOX_API_KEY = os.environ["SCARNOX_API_KEY"]
SCARNOX_TIMEOUT = 185

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.PopularCaptchaMiddleware": 543,
}

ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3
CONCURRENT_REQUESTS_PER_DOMAIN = 1

Jeda download dan konkurensi tunggal per domain memang sengaja diatur. Keduanya menjaga beban situs tetap rendah dan mengurangi jumlah captcha yang Anda temui.

Middleware

# myproject/middlewares.py
import requests
from scrapy import Request
from scrapy.http import Response
from twisted.internet.threads import deferToThread

API_URL = "https://api.scarnox.com/api/tasks/create"


class PopularCaptchaMiddleware:
    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            api_key=crawler.settings.get("SCARNOX_API_KEY"),
            timeout=crawler.settings.getint("SCARNOX_TIMEOUT", 185),
        )

    def _solve(self, sitekey: str, page_url: str) -> str:
        r = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={
                "captcha_type": "PopularCaptchaTaskProxyless",
                "site_key": sitekey,
                "site_url": page_url,
            },
            timeout=self.timeout,  # berjalan di thread worker, bukan di reactor
        )
        data = r.json()
        if data.get("errorId") != 0:
            raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
        return data["solution"]["token"]

    def process_response(self, request: Request, response: Response, spider):
        # Sudah dipecahkan, atau pengiriman form yang kita buat sendiri: teruskan.
        if request.meta.get("captcha_token") or request.meta.get("skip_captcha"):
            return response

        sitekey = response.css("[data-sitekey]::attr(data-sitekey)").get()
        if not sitekey:
            return response

        spider.logger.info("PopularCaptcha pada %s, sedang memecahkan", response.url)

        def retry_with_token(token: str) -> Request:
            # Mengembalikan Request berarti menjadwalkannya ulang. dont_filter melewati dupe filter.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

        return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)

Mengembalikan Deferred dari process_response didukung oleh Scrapy. Callback mengubah token menjadi request yang di-retry, dan Scrapy menjadwalkannya seperti request biasa.

Spider

# myproject/spiders/search.py
import scrapy
from scrapy.http import FormRequest


class SearchSpider(scrapy.Spider):
    name = "search"
    start_urls = ["https://search.example.com/search?q=web+scraping"]

    def parse(self, response):
        token = response.meta.get("captcha_token")
        if token and response.css("[data-sitekey]"):
            # Scrapy mengisi field form milik halaman; kita menambahkan token.
            yield FormRequest.from_response(
                response,
                formdata={
                    "h-captcha-response": token,
                    "g-recaptcha-response": token,
                },
                callback=self.parse_results,
                meta={"skip_captcha": True},
                dont_filter=True,
            )
            return

        yield from self.parse_results(response)

    def parse_results(self, response):
        for item in response.css("div.result"):
            yield {
                "title": item.css("h3::text").get(),
                "url": item.css("a::attr(href)").get(),
            }

Selector-nya hanyalah placeholder. Ganti dengan selector sebenarnya dari situs yang Anda scrape.

Flag skip_captcha adalah yang mencegah middleware memecahkan pengiriman form lagi. Tanpanya, setiap pengiriman akan menimbulkan biaya solve tambahan.

Jalankan

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json

Perhatikan log. Baris seperti PopularCaptcha pada ..., sedang memecahkan berarti tantangan ditemukan dan token diminta. Jika Anda melihat baris itu di setiap halaman, perlambat. Captcha yang sering muncul adalah tanda bahwa situs telah menyadari crawl tersebut.

Waktu token

Token PopularCaptcha kedaluwarsa dalam sekitar dua menit. Middleware meminta retry segera setelah token tiba, dan spider mengirim di rangkaian callback yang sama. Jaga rangkaian itu tetap pendek. Jangan menambahkan download yang lambat atau pemrosesan panjang di antara token dan pengiriman form. Untuk penjelasan lebih lanjut tentang jendela waktu tersebut, lihat masa berlaku token captcha.

Error

Solve yang gagal memunculkan RuntimeError di dalam thread. Scrapy akan mencatatnya sebagai request yang gagal. Solve yang gagal tidak ditagih, jadi retry itu murah. Untuk kebijakan retry dengan batas, gunakan RetryMiddleware bawaan Scrapy dan bungkus _solve dalam loop pendek. Panduan kami tentang penanganan error solver captcha mencantumkan pola yang tahan di produksi.

Scraping secara bertanggung jawab

Untuk latar belakang hukumnya, lihat apakah web scraping legal.

Harga

Scarnox menagih per solve yang berhasil, dan solve yang gagal gratis. Lihat harga terkini untuk tarifnya. Mulai dengan kredit uji coba gratis, tanpa kartu.

Langkah selanjutnya