Un scraper rencontre une réponse lente, alors il relance. La relance échoue aussi, alors il relance encore, immédiatement, et il en va de même pour tous les autres workers qui ont heurté le même mur au même moment. En quelques secondes, vous avez envoyé une rafale de trafic vers un site qui vous disait déjà d’en envoyer moins, et ce qui a commencé comme une limitation temporaire est désormais un blocage dur sur chaque adresse impliquée. Le site n’a pas escaladé. Votre boucle de relance l’a fait.
C’est la façon la plus courante dont un scraper bien construit se blesse lui-même, et c’est entièrement évitable. La logique de relance est un mécanisme de délestage de charge, pas un mécanisme de persistance, et la différence entre ces deux idées est la différence entre un job qui se dégrade gracieusement et un job qui se fait bannir. L’hygiène plus générale est couverte dans éviter les blocages ; ceci porte sur la mécanique du chemin de relance lui-même.
Pourquoi les relances naïves aggravent les choses
Trois dynamiques se combinent, et elles se combinent ensemble.
La première est que les relances ajoutent de la charge exactement quand la charge est le problème. Un 429 ou une réponse lente est une demande d’envoyer moins, et y répondre par plus de requêtes inverse le signal. La deuxième est la synchronisation. Des workers qui échouent au même moment et attendent le même intervalle fixe relanceront aussi au même moment, donc au lieu de s’étaler, ils arrivent sous forme de rafale coordonnée, et chaque round de cette rafale resynchronise le suivant. La troisième est que les relances sont généralement comptabilisées contre vous par adresse, donc marteler une cible depuis la même IP fait passer cette adresse de limitée à signalée, ce qui est un problème de réputation qui survit à l’incident et suit l’adresse jusqu’à votre prochain job.
Mis bout à bout, une boucle naïve prend une condition récupérable et produit exactement la forme de trafic que les systèmes anti-bot sont conçus pour attraper. Le même échec, géré avec retenue, se serait résolu de lui-même.
Classifier avant de relancer
La première règle est que tout échec ne mérite pas une relance, et ceux qui en méritent une méritent des relances différentes. Triez les réponses en trois catégories.
Certains échecs sont transitoires et valent la peine d’être relancés : réinitialisations de connexion, timeouts, 502, 503, 504, et 429. Ceux-ci représentent un système momentanément incapable plutôt que réticent, et le 429 en particulier est une instruction explicite sur le rythme plutôt qu’un refus. Certains sont définitifs et ne doivent jamais être relancés : 404, 400, 401, 403 qui persiste sur plusieurs adresses, et une page qui s’est parsée proprement mais ne contenait rien de ce que vous vouliez. Relancer ceux-ci brûle de la bande passante et de la réputation pour un résultat qui ne peut pas changer, et un échec de parsing est un bug de code qu’une relance reproduira fidèlement pour toujours.
La troisième catégorie est la dangereuse : les réponses qui ont l’air réussies et ne le sont pas. Une page de challenge, un résultat générique ou vide, une liste tronquée, ou une redirection vers une page d’atterrissage peuvent tous arriver avec un statut 200, et un scraper qui fait confiance uniquement aux codes de statut les enregistrera joyeusement comme des données. Validez le corps de la réponse avant de compter une réponse comme un succès, ce qui est l’objet de détecter un contenu bloqué ou factice. Un blocage doux est un échec qui peut être relancé, mais seulement si vous remarquez qu’il s’agit d’un échec.
Le backoff exponentiel, et pourquoi le jitter n’est pas optionnel
Pour la catégorie relançable, le délai entre les tentatives doit croître, et la forme standard est exponentielle : attendre une seconde, puis deux, puis quatre, puis huit. La croissance compte parce qu’elle donne progressivement plus de marge à une cible en difficulté au lieu d’un tambourinage constant.
Mais le backoff exponentiel seul ne suffit pas, et c’est la partie que les gens sautent. Si cent workers échouent ensemble et attendent tous exactement une seconde, ils relancent tous ensemble une seconde plus tard. Le backoff a augmenté, mais la rafale a survécu, et vous avez simplement déplacé le même pic plus loin dans le temps. La solution est le jitter : randomiser chaque délai plutôt que d’utiliser directement la valeur calculée. Le full jitter, c’est-à-dire une attente aléatoire choisie entre zéro et le plafond actuel, étale un échec synchronisé en une distribution lisse de relances. C’est un changement de deux lignes et c’est la chose la plus efficace de cet article.
Deux autres règles vont avec cela. Respectez Retry-After quand un site en envoie un, car cet en-tête est la cible qui vous dit exactement combien de temps attendre, et l’ignorer en faveur de votre propre calendrier est à la fois discourtois et contre-productif. Et plafonnez à la fois le délai et le nombre de tentatives, car une requête qui a échoué cinq fois n’a pas de raison de réussir à la sixième, et une relance sans limite n’est qu’une façon lente de ne jamais abandonner quelque chose qui est déjà perdu.
import random, time
import requests
PROXY = "http://customer-USERNAME-country-us:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
TRANSIENT = {429, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE, CAP = 1.0, 60.0
def fetch(url):
for attempt in range(MAX_ATTEMPTS):
try:
r = requests.get(url, proxies=PROXIES, timeout=20)
except requests.RequestException:
pass # transient: fall through to backoff
else:
if r.status_code == 200 and is_valid(r.text):
return r.text # validate the body, not just the code
if r.status_code not in TRANSIENT:
return None # terminal: do not retry
after = r.headers.get("Retry-After")
if after:
time.sleep(min(float(after), CAP)) # the target told you the answer
continue
ceiling = min(CAP, BASE * (2 ** attempt))
time.sleep(random.uniform(0, ceiling)) # full jitter, not a fixed delay
return None
Tourner ou attendre : la décision que les relances prennent souvent mal
Avec les proxys résidentiels, il existe un second axe. Un échec peut être traité par du temps, par une adresse différente, ou par les deux, et se tromper de choix gaspille l’un des deux.
Attendez quand le signal porte sur le rythme. Un 429 ou un Retry-After est la cible qui dit que votre cadence est trop élevée, et passer à une adresse fraîche pour pouvoir garder la même cadence est précisément le comportement qui ressemble à de l’évasion et qui fait bannir tout un pool plutôt qu’une seule route. Ralentissez plutôt.
Tournez quand le signal porte sur l’adresse. Une page de blocage, un 403 persistant, ou un challenge qui continue d’apparaître sur une route signifie que cette adresse spécifique n’est plus digne de confiance, et attendre ne la restaurera pas. Retirez la route et continuez sur une adresse fraîche, ce qui est le modèle de failover, et notez que la réputation du remplaçant est ce qui détermine si la relance aide réellement. Le seul cas où la rotation est erronée est le travail en milieu de session : si un flux dépend d’une identité maintenue, changer d’adresse le casse, donc un échec au sein d’une session persistante signifie redémarrer la séquence sur une nouvelle session plutôt que de changer d’adresse sous la session existante.
Les timeouts se situent entre les deux et méritent leur propre diagnostic plutôt qu’un réflexe, puisque les raisons pour lesquelles les requêtes expirent incluent la lenteur de la cible, une route défaillante, et votre propre concurrence trop élevée.
Budgets et disjoncteurs
Les règles de relance par requête ne suffisent pas à elles seules, car elles n’ont aucune vue sur le système. Deux mécanismes vous donnent cette vue.
Un budget de relance plafonne les relances comme une proportion du trafic total, par exemple en autorisant les relances à représenter au maximum dix pour cent des requêtes vers une cible donnée. En conditions normales, le budget n’est jamais touché. Quand quelque chose casse largement, le budget est épuisé immédiatement et les relances supplémentaires ne se produisent tout simplement pas, ce qui est la propriété que vous voulez : les relances aident en cas d’échecs isolés et sont activement nuisibles pendant une panne générale, et un budget est ce qui fait automatiquement la différence.
Un disjoncteur va plus loin. Suivez le taux d’échec par cible, et lorsqu’il dépasse un seuil, arrêtez complètement d’envoyer vers cette cible pendant une période de refroidissement plutôt que de continuer à la sonder avec un filet de requêtes vouées à l’échec. Après le refroidissement, laissez passer un petit nombre de requêtes, et si elles réussissent, reprenez. Cela protège la cible de votre accumulation et protège vos adresses d’accumuler des échecs contre un site qui ne répond à personne en ce moment. Les deux mécanismes sont par cible plutôt que globaux, car un site cassé ne devrait jamais arrêter un job qui collecte auprès de cinquante autres.
Les relances coûtent de l’argent et se cachent dans vos données
Deux conséquences qui valent la peine d’être énoncées clairement.
Chaque relance est une requête que vous payez. Sur des proxys résidentiels tarifés à la bande passante, une tempête de relances est un poste de coût, et un job qui relance discrètement cinq fois contre un site qui est en panne tout l’après-midi peut déplacer une quantité surprenante de données pour rien, ce qui est l’un des contributeurs les moins évidents au tableau des coûts dans réduire les coûts de bande passante des proxys.
Et le taux de relance est un indicateur avancé qui a sa place sur votre tableau de bord. Un taux de relance en hausse sur une cible est le premier avertissement que ses défenses ont changé ou que votre cadence a dérivé, et cela apparaît bien avant que le taux de succès ne s’effondre, donc surveiller le pipeline devrait suivre les tentatives et les résultats plutôt que seulement les résultats finaux. Un scraper qui relance silencieusement jusqu’à obtenir un taux de succès d’apparence normale cache le problème, il ne le résout pas.
Enfin, quand une requête épuise ses tentatives, ne la jetez pas. Poussez-la vers une file d’attente de lettres mortes et relancez-la beaucoup plus tard, lors du prochain run ou après un long refroidissement, plutôt que dans la rafale en cours. La plupart des éléments qui échouent pendant un incident réussissent d’eux-mêmes une heure plus tard, et une file d’attente transforme un échec dur en échec différé.
L’essentiel
La logique de relance existe pour absorber les échecs transitoires, pas pour insister. Classifiez l’échec avant d’agir, ne relancez que ce qui est véritablement transitoire, et validez les corps de réponse pour qu’un blocage doux soit traité comme l’échec qu’il est. Reculez de façon exponentielle et toujours avec du jitter, respectez Retry-After, et plafonnez à la fois le délai et le nombre de tentatives. Distinguez les signaux de rythme, qui appellent à attendre, des signaux d’adresse, qui appellent à tourner, et ne répondez jamais à une limitation en changeant d’adresses pour maintenir la même cadence. Ajoutez un budget de relance et un disjoncteur par cible afin qu’une panne large ne puisse pas se transformer en déluge auto-infligé. Faites cela, et la plupart des bannissements que les équipes attribuent à une escalade anti-bot cessent simplement de se produire, parce qu’il ne s’agissait jamais d’une escalade en premier lieu.
L’autre moitié consiste à avoir un endroit où basculer, ce que fournissent les proxys résidentiels : un large pool d’IP résidentielles réelles, de sorte qu’une route retirée soit remplacée par une adresse propre plutôt que par la même adresse qui réessaie. La tarification au Go est aussi la raison pour laquelle des relances disciplinées se rentabilisent d’elles-mêmes, puisque vous ne payez que pour les requêtes que vous effectuez réellement, et une tempête que vous n’envoyez jamais est de la bande passante que vous n’achetez jamais.