Scarnox · Blog

Middleware captcha per Scrapy: risolvi i captcha nel tuo spider (2026)

Scrapy è costruito attorno a richieste e callback, quindi un captcha non si incastra bene al suo interno. Il modo pulito per gestirlo è un middleware del downloader: vede ogni risposta, nota una sfida, ottiene un token e lascia che lo spider invii il modulo con quel token. Questa guida costruisce quel middleware per le pagine PopularCaptcha, che Scarnox risolve oggi in modalità proxyless.

Inizia con il credito di prova gratuito, senza carta e prova il codice su una pagina di test prima di puntarlo verso qualsiasi altro sito.

Come funziona il middleware

Il middleware viene eseguito su ogni risposta:

  1. Se la risposta non ha un data-sitekey, lascia passare la risposta.
  2. Se ce l'ha, il middleware risolve il captcha e restituisce una copia della stessa richiesta con il token memorizzato in meta.
  3. Scrapy esegue di nuovo quella richiesta. Il middleware vede il token in meta e lascia passare la risposta.
  4. Lo spider legge il token da meta e invia il modulo con FormRequest.from_response.

La chiamata di risoluzione viene eseguita in un thread tramite deferToThread, quindi non blocca il reactor di Scrapy. Le altre richieste continuano a procedere mentre il captcha viene risolto.

Impostazioni del progetto

Metti la tua chiave in una variabile d'ambiente, non nel codice:

# 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

Il ritardo di download e la concorrenza singola per dominio sono lì apposta. Mantengono basso il carico sul sito e riducono il numero di sfide che incontri.

Il 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,  # gira in un thread di lavoro, non nel 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):
        # Già risolto, oppure un invio del modulo che abbiamo costruito noi: lascia passare.
        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:
            # Restituire una Request la rimette in coda. dont_filter evita il filtro dei duplicati.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

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

Restituire un Deferred da process_response è supportato da Scrapy. La callback trasforma il token in una richiesta da ritentare, e Scrapy la pianifica come qualsiasi altra.

Lo 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 compila i campi del modulo; noi aggiungiamo il 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(),
            }

I selettori sono segnaposto. Sostituiscili con quelli reali del sito di cui fai scraping.

Il flag skip_captcha è ciò che impedisce al middleware di risolvere di nuovo l'invio del modulo. Senza di esso, ogni invio costerebbe un'altra risoluzione.

Esegui

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

Osserva il log. Una riga come PopularCaptcha on ..., solving indica che è stata trovata una sfida e che è stato richiesto un token. Se vedi quella riga su ogni pagina, rallenta. Le sfide frequenti sono un segnale che il sito ha notato il crawl.

Tempistica del token

Un token PopularCaptcha scade in circa due minuti. Il middleware richiede il nuovo tentativo non appena arriva il token, e lo spider invia nella stessa catena di callback. Mantieni la catena breve. Non aggiungere download lenti o elaborazioni lunghe tra il token e l'invio del modulo. Per approfondire la finestra, vedi scadenza del token captcha.

Errori

Una risoluzione fallita genera un RuntimeError all'interno del thread. Scrapy la registrerà come richiesta fallita. Le risoluzioni fallite non vengono addebitate, quindi ritentare costa poco. Per una politica di nuovi tentativi con limiti, usa il RetryMiddleware integrato di Scrapy e avvolgi _solve in un breve ciclo. La nostra guida sulla gestione degli errori del solver captcha elenca i pattern che reggono in produzione.

Scraping responsabile

Per il quadro legale, vedi il web scraping è legale?.

Tariffe

Scarnox addebita per ogni risoluzione riuscita, e le risoluzioni fallite non vengono addebitate. Consulta le tariffe attuali per i prezzi. Inizia con il credito di prova gratuito, senza carta.

Passi successivi