Scarnox · Blog

Scrapy captcha-middleware: captchas oplossen in je spider (2026)

Scrapy is opgebouwd rond requests en callbacks, dus een captcha past daar niet netjes in. De nette manier om er een af te handelen is een downloader-middleware: die ziet elke response, merkt een uitdaging op, haalt een token op, en laat de spider het formulier met dat token versturen. Deze gids bouwt die middleware voor PopularCaptcha-pagina's, die Scarnox vandaag oplost op een proxyless basis.

Begin met gratis proefkrediet, zonder creditcard en probeer de code op een testpagina voordat je hem op iets anders richt.

Hoe de middleware werkt

De middleware draait op elke response:

  1. Als de response geen data-sitekey heeft, geeft hij de response door.
  2. Als dat wel zo is, lost de middleware de captcha op en geeft hij een kopie van hetzelfde request terug, met het token opgeslagen in meta.
  3. Scrapy haalt dat request opnieuw op. De middleware ziet het token in meta en laat de response door.
  4. De spider leest het token uit meta en verstuurt het formulier met FormRequest.from_response.

De solve-aanroep draait in een thread via deferToThread, dus hij blokkeert de reactor van Scrapy niet. Andere requests blijven doorlopen terwijl de captcha wordt opgelost.

Projectinstellingen

Zet je sleutel in een omgevingsvariabele, niet in de code:

# 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

De download-vertraging en de enkele concurrency per domein staan er met een reden. Ze houden de belasting van de site laag en verminderen het aantal captchas dat je ziet.

De 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,  # draait in een worker-thread, niet in de 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):
        # Al opgelost, of een formulierverzending die we zelf hebben gebouwd: doorlaten.
        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 on %s, solving", response.url)

        def retry_with_token(token: str) -> Request:
            # Een Request teruggeven plant het opnieuw in. dont_filter omzeilt het duplicaatfilter.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

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

Scrapy ondersteunt het teruggeven van een Deferred vanuit process_response. De callback maakt van het token een opnieuw uitgevoerd request, en Scrapy plant het in zoals elk ander request.

De 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 vult de eigen velden van het formulier in; wij voegen het token toe.
            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(),
            }

De selectors zijn placeholders. Vervang ze door de echte van de site die je scrapet.

De vlag skip_captcha zorgt ervoor dat de middleware de formulierverzending niet opnieuw oplost. Zonder die vlag zou elke verzending nog een solve kosten.

Draaien

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

Houd de log in de gaten. Een regel zoals PopularCaptcha op ..., wordt opgelost betekent dat er een uitdaging is gevonden en dat er een token is aangevraagd. Als je die regel op elke pagina ziet, vertraag dan. Veelvuldige uitdagingen zijn een teken dat de site je crawl heeft opgemerkt.

Timing van het token

Een PopularCaptcha-token verloopt na ongeveer twee minuten. De middleware vraagt het opnieuwgeplande request aan zodra het token binnen is, en de spider verstuurt in dezelfde keten van callbacks. Houd de keten kort. Voeg geen trage downloads of lange verwerking toe tussen het token en het verzenden van het formulier. Voor meer over dat venster, zie verlopen captchatokens.

Fouten

Een mislukte solve gooit een RuntimeError binnen de thread. Scrapy logt dat als een mislukt request. Mislukte solves worden niet in rekening gebracht, dus opnieuw proberen is goedkoop. Voor een retrybeleid met limieten gebruik je de ingebouwde RetryMiddleware van Scrapy en omring je _solve met een korte lus. Onze gids over captcha solver-foutafhandeling somt de patronen op die in productie standhouden.

Scrape verantwoord

Voor de juridische achtergrond, zie is webscraping legaal.

Prijzen

Scarnox rekent per geslaagde solve, en mislukte solves zijn gratis. Zie actuele prijzen voor de tarieven. Begin met gratis proefkrediet, zonder creditcard.

Volgende stappen