Scarnox · Blog

Middleware de captcha para Scrapy: resolva captchas no seu spider (2026)

O Scrapy é construído em torno de requisições e callbacks, então um captcha não se encaixa bem nele. A forma limpa de lidar com um é um middleware de downloader: ele vê cada resposta, percebe um desafio, obtém um token e deixa o spider enviar o formulário com esse token. Este guia constrói esse middleware para páginas PopularCaptcha, que a Scarnox resolve hoje em modo proxyless.

Comece com crédito de teste grátis, sem cartão e teste o código em uma página de teste antes de apontá-lo para qualquer outro lugar.

Como o middleware funciona

O middleware roda em cada resposta:

  1. Se a resposta não tem data-sitekey, ele deixa a resposta passar.
  2. Se tem, o middleware resolve o captcha e devolve uma cópia da mesma requisição, com o token guardado em meta.
  3. O Scrapy busca essa requisição de novo. O middleware vê o token em meta e deixa a resposta passar.
  4. O spider lê o token de meta e envia o formulário com FormRequest.from_response.

A chamada de resolução roda em uma thread através de deferToThread, então não bloqueia o reactor do Scrapy. Outras requisições continuam andando enquanto o captcha é resolvido.

Configurações do projeto

Guarde a sua chave em uma variável de ambiente, e não no código:

# 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

O atraso de download e a concorrência única por domínio estão aí de propósito. Eles mantêm baixa a carga sobre o site e reduzem o número de desafios que você enfrenta.

O 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,  # roda em uma thread de trabalho, não no 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):
        # Já resolvido, ou um envio de formulário que criamos nós: deixe passar.
        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:
            # Devolver uma Request a reagenda. dont_filter evita o filtro de duplicatas.
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

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

O Scrapy aceita retornar um Deferred de process_response. O callback transforma o token em uma requisição de nova tentativa, e o Scrapy a agenda como qualquer outra.

O 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]"):
            # O Scrapy preenche os campos do próprio formulário; nós adicionamos o 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(),
            }

Os seletores são exemplos. Troque-os pelos reais do site que você está raspando.

A flag skip_captcha é o que impede o middleware de resolver o envio do formulário de novo. Sem ela, cada envio custaria outra resolução.

Execute

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

Acompanhe o log. Uma linha como PopularCaptcha on ..., solving significa que um desafio foi encontrado e um token foi solicitado. Se você vir essa linha em todas as páginas, desacelere. Desafios frequentes são sinal de que o site percebeu o rastreamento.

Tempo do token

Um token PopularCaptcha expira em cerca de dois minutos. O middleware solicita a nova tentativa assim que o token chega, e o spider envia na mesma cadeia de callbacks. Mantenha a cadeia curta. Não adicione downloads lentos nem processamento longo entre o token e o envio do formulário. Para mais detalhes sobre essa janela, veja expiração de token de captcha.

Erros

Uma resolução que falha levanta RuntimeError dentro da thread. O Scrapy registra isso como uma requisição com falha. Resoluções que falham não são cobradas, então tentar de novo é barato. Para uma política de novas tentativas com limites, use o RetryMiddleware embutido do Scrapy e envolva _solve em um laço curto. Nosso guia sobre tratamento de erros do solver de captcha lista os padrões que se sustentam em produção.

Faça scraping com responsabilidade

Para o contexto jurídico, veja a web scraping é legal.

Preços

A Scarnox cobra por resolução bem-sucedida, e resoluções que falham são gratuitas. Veja a página de preços para as tarifas. Comece com crédito de teste grátis, sem cartão.

Próximos passos