Middleware de captcha para Scrapy: resuelve captchas en tu spider (2026)
Scrapy está construido en torno a peticiones y callbacks, así que un captcha no encaja del todo en él. La forma limpia de gestionarlo es un middleware de descarga: ve cada respuesta, detecta un desafío, obtiene un token y deja que el spider envíe el formulario con ese token. Esta guía construye ese middleware para páginas de PopularCaptcha, que Scarnox resuelve hoy sin proxy.
Empieza con crédito de prueba gratis, sin tarjeta y prueba el código contra una página de prueba antes de apuntarlo a cualquier otra cosa.
Cómo funciona el middleware
El middleware se ejecuta en cada respuesta:
- Si la respuesta no tiene
data-sitekey, deja pasar la respuesta. - Si lo tiene, el middleware resuelve el captcha y devuelve una copia de la misma petición con el token guardado en
meta. - Scrapy vuelve a obtener esa petición. El middleware ve el token en
metay deja pasar la respuesta. - El spider lee el token de
metay envía el formulario conFormRequest.from_response.
La llamada de resolución se ejecuta en un hilo mediante deferToThread, así que no bloquea el reactor de Scrapy. Las demás peticiones siguen avanzando mientras se resuelve el captcha.
Ajustes del proyecto
Pon tu clave en una variable de entorno, no en el 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
El retardo de descarga y la concurrencia única por dominio están ahí a propósito. Mantienen baja la carga del sitio y reducen el número de desafíos que ves.
El 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, # se ejecuta en un hilo de trabajo, no en el 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):
# Ya resuelta, o un envío de formulario que construimos nosotros: deja pasar.
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 una Request la reprograma. dont_filter evita el filtro de duplicados.
return request.replace(
dont_filter=True,
meta={**request.meta, "captcha_token": token},
)
return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)
Scrapy admite devolver un Deferred desde process_response. El callback convierte el token en una petición reintentada, y Scrapy la programa como cualquier otra.
El 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 rellena los propios campos del formulario; nosotros añadimos el 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(),
}
Los selectores son marcadores de posición. Sustitúyelos por los reales del sitio que estés scrapeando.
La marca skip_captcha es lo que impide que el middleware vuelva a resolver el envío del formulario. Sin ella, cada envío costaría otra resolución.
Ejecútalo
export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json
Vigila el registro. Una línea como PopularCaptcha on ..., solving significa que se encontró un desafío y se solicitó un token. Si ves esa línea en cada página, ve más despacio. Los desafíos frecuentes son señal de que el sitio ha detectado el rastreo.
Tiempos del token
Un token de PopularCaptcha caduca en unos dos minutos. El middleware solicita el reintento en cuanto llega el token, y el spider envía en la misma cadena de callbacks. Mantén la cadena corta. No añadas descargas lentas ni procesamiento largo entre el token y el envío del formulario. Para más detalles sobre esa ventana, consulta caducidad del token de captcha.
Errores
Una resolución fallida lanza un RuntimeError dentro del hilo. Scrapy lo registrará como una petición fallida. Las resoluciones fallidas no se cobran, así que reintentar es barato. Para una política de reintentos con límites, usa el RetryMiddleware integrado de Scrapy y envuelve _solve en un bucle corto. Nuestra guía sobre gestión de errores del solver de captcha enumera los patrones que aguantan en producción.
Scrapea de forma responsable
- Respeta el robots.txt.
ROBOTSTXT_OBEY = Truede arriba hace esto. Mantenlo activado. - Mantén el retardo alto. Un desafío es señal de que el sitio quiere que vayas más despacio. No eludas el retardo para terminar antes.
- Lee los términos de servicio antes de rastrear. Algunos sitios prohíben el acceso automatizado de forma directa.
- Gestiona los datos personales con cuidado. Guarda solo los campos que necesitas, y protege lo que conservas.
Para el trasfondo legal, consulta si el web scraping es legal.
Precios
Scarnox cobra por resolución exitosa, y las resoluciones fallidas son gratuitas. Consulta las tarifas actuales para ver los precios. Empieza con crédito de prueba gratis, sin tarjeta.