Middleware captcha pour Scrapy : résoudre les captchas dans votre spider (2026)
Scrapy repose sur les requêtes et les callbacks, donc un captcha ne s'intègre pas naturellement dans son fonctionnement. La bonne façon de le gérer est un middleware de téléchargement : il voit chaque réponse, repère un défi, obtient un jeton, et laisse le spider soumettre le formulaire avec ce jeton. Ce guide construit ce middleware pour les pages PopularCaptcha, que Scarnox résout aujourd'hui sans proxy.
Commencez avec un crédit d'essai gratuit, sans carte bancaire et testez le code sur une page de test avant de le lancer sur quoi que ce soit d'autre.
Fonctionnement du middleware
Le middleware s'exécute sur chaque réponse :
- Si la réponse ne contient pas de
data-sitekey, il laisse passer la réponse. - Si elle en contient un, le middleware résout le captcha et renvoie une copie de la même requête, avec le jeton stocké dans
meta. - Scrapy relance cette requête. Le middleware voit le jeton dans
metaet laisse passer la réponse. - Le spider lit le jeton dans
metaet soumet le formulaire avecFormRequest.from_response.
L'appel de résolution s'exécute dans un thread via deferToThread, il ne bloque donc pas le reactor de Scrapy. Les autres requêtes continuent d'avancer pendant que le captcha est résolu.
Réglages du projet
Placez votre clé dans une variable d'environnement, et non dans le 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
Le délai de téléchargement et la concurrence limitée à une requête par domaine sont là volontairement. Ils gardent faible la charge sur le site et réduisent le nombre de captchas rencontrés.
Le 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, # s'exécute dans un thread de travail, pas dans le 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):
# Déjà résolu, ou soumission de formulaire que nous avons construite nous-mêmes : laisser passer.
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:
# Renvoyer une Request la reprogramme. dont_filter évite le filtre de doublons.
return request.replace(
dont_filter=True,
meta={**request.meta, "captcha_token": token},
)
return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)
Scrapy accepte de renvoyer un Deferred depuis process_response. Le callback transforme le jeton en requête relancée, et Scrapy la programme comme n'importe quelle autre.
Le 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 remplit les champs propres au formulaire ; nous ajoutons le jeton.
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(),
}
Les sélecteurs sont des exemples. Remplacez-les par les vrais, issus du site que vous scrapez.
Le drapeau skip_captcha empêche le middleware de résoudre à nouveau la soumission du formulaire. Sans lui, chaque envoi coûterait une nouvelle résolution.
Lancer le spider
export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json
Surveillez le journal. Une ligne du type PopularCaptcha on ..., solving signifie qu'un défi a été trouvé et qu'un jeton a été demandé. Si vous voyez cette ligne sur chaque page, ralentissez. Des défis fréquents indiquent que le site a repéré votre crawl.
Le timing du jeton
Un jeton PopularCaptcha expire au bout d'environ deux minutes. Le middleware demande la relance dès que le jeton arrive, et le spider soumet dans la même chaîne de callbacks. Gardez cette chaîne courte. N'ajoutez ni téléchargements lents ni traitements longs entre le jeton et l'envoi du formulaire. Pour en savoir plus sur cette fenêtre, voir expiration du jeton de captcha.
Erreurs
Une résolution échouée lève une RuntimeError dans le thread. Scrapy la journalise comme une requête en échec. Les résolutions échouées ne sont pas facturées, donc réessayer ne coûte presque rien. Pour une politique de nouvelles tentatives avec des limites, utilisez le RetryMiddleware intégré de Scrapy et enveloppez _solve dans une courte boucle. Notre guide sur la gestion des erreurs du solveur de captcha présente les schémas qui tiennent en production.
Scraper de manière responsable
- Respectez le robots.txt.
ROBOTSTXT_OBEY = Trueci-dessus s'en charge. Gardez-le activé. - Gardez le délai élevé. Un défi est le signe que le site souhaite que vous ralentissiez. Ne contournez pas le délai pour aller plus vite.
- Lisez les conditions d'utilisation avant de crawler. Certains sites interdisent purement et simplement l'accès automatisé.
- Traitez les données personnelles avec soin. Ne stockez que les champs dont vous avez besoin, et protégez ce que vous conservez.
Pour le contexte juridique, voir le web scraping est-il légal.
Tarifs
Scarnox facture à chaque résolution réussie, et les résolutions échouées sont gratuites. Consultez les tarifs actuels pour les montants. Commencez avec un crédit d'essai gratuit, sans carte bancaire.