La plupart des rotateurs de proxy répondent à la mauvaise question. Ils répondent à "à qui le tour ?" alors que la question qui décide si votre pipeline arrive au bout est "quelle route doit porter cette requête ?" Le round-robin donne à chaque route une part égale du trafic, ce qui n'est correct que si toutes les routes se valent. Les pools ne sont jamais aussi propres : une route est rapide jusqu'à ce qu'elle soit bloquée, une autre est lente mais ne tombe jamais, une troisième tombe par salves puis se rétablit.
Une fois cela admis, la rotation cesse d'être un problème d'ordonnancement pour devenir un problème de rétroaction. Le rotateur observe ce qui arrive à chaque requête, tient une estimation courante du comportement de chaque route, et laisse ces estimations guider le choix suivant. C'est toute l'idée du health scoring : le pool vous dit ce qu'il fait, et la politique de routage écoute.
Ce guide construit ce rotateur en Python. Des estimations à pondération exponentielle suivent le succès et la latence, un circuit breaker gère les défaillances brutales pour lesquelles les moyennes sont trop lentes, et un benchmark mesure le résultat face à un round-robin simple. L'une des routes est le Crawlbase Smart AI Proxy, qui mérite sa place précisément parce qu'il absorbe toute une couche du problème : vous cessez de noter des IP résidentielles individuelles pour noter une route qui gère elle-même la rotation et la résolution anti-bot.
- Séparez l'exécution, la santéet la sélection. Une route récupère, le modèle de santé note, la politique choisit. Chaque partie peut être remplacée ou évaluée seule.
- Deux EWMA portent le signal continu : une pour le succès, une pour la latence. Ne mettez à jour la latence que sur les succès, car une requête en échec ne dit rien de la rapidité d'une route quand elle fonctionne.
- Un circuit breaker fait ce qu'une moyenne ne peut pas : une salve d'échecs consécutifs doit retirer une route tout de suite, pas progressivement.
- La santé se multiplie, elle ne s'additionne pas :
success_ewma / (1 + latency_ewma). Une route doit être à la fois fiable et réactive pour bien scorer. - Gardez un exposant de sélection modeste pour qu'une route en cours de rétablissement reçoive encore du trafic occasionnel et puisse prouver qu'elle est guérie.
- Le round-robin est le témoin, pas un épouvantail. Sans lui, vous ne pouvez pas montrer que la boucle de rétroaction a servi à quelque chose.
Pourquoi la rotation a besoin de health scoring
Un pool de proxys n'est pas homogène, et il ne reste pas immobile. Les routes diffèrent en latence, en fiabilité et dans la façon dont une cible donnée les traite, et ces trois choses peuvent changer pendant qu'un job tourne. Les traiter comme interchangeables rend la politique aveugle aux seules conditions qui comptent.
Un rotateur conscient de la santé a besoin de trois signaux, et ce n'est pas le même signal à des sensibilités différentes :
- Vivacité. Quelles routes réussissent en ce moment, et desquelles le trafic doit s'éloigner.
- Latence. Parmi les routes qui fonctionnent, préférez les réactives. Une requête réussie qui a pris neuf secondes vous a quand même coûté neuf secondes.
- Stabilité des échecs. Des échecs répétés doivent sortir une route du trafic normal et la laisser revenir par une sonde contrôlée, pas par un retour immédiat dans le pool.
Le round-robin n'en fournit aucun. Il répartit les requêtes uniformément, que la route soit saine, lente ou morte. C'est exactement ce qui en fait le bon témoin : lancez les deux politiques sur la même charge, et l'écart est la valeur de la rétroaction.
La forme du système
Trois responsabilités, tenues à l'écart les unes des autres à dessein. Une route effectue une récupération et rapporte un résultat normalisé : réussi ou non, statut HTTP, durée. Un modèle de santé transforme ce flux de résultats en score. Une politique lit les scores et choisit la route suivante. Parce qu'elles sont séparées, vous pouvez remplacer la politique sans toucher au transport, ou comparer deux politiques sur des routes identiques.
Le Smart AI Proxy gagne sa place dans ce schéma en absorbant une couche. Il présente un seul point d'entrée et gère derrière lui la rotation d'IP résidentielles et la résolution anti-bot, si bien que votre application note une route plutôt qu'une flotte d'adresses. Votre politique décide toujours quel chemin de récupération mérite le trafic.
Chaque extrait ci-dessous provient du dépôt compagnon ScraperHub/smart-ai-proxy-rotation-in-python-health-scoring-at-scale, qui contient l'implémentation exécutable sous final/ et des points d'étape sous steps/.
Environnement
Python 3.11 ou plus récent, et un compte Crawlbase pour la route proxifiée. Votre Normal token suffit ici ; le JavaScript token n'intervient que lorsqu'une cible a besoin d'un rendu pour produire du contenu, ce qui est une question distincte du routage. Les deux se trouvent dans les paramètres de la console.
git clone https://github.com/ScraperHub/smart-ai-proxy-rotation-in-python-health-scoring-at-scale.git cd smart-ai-proxy-rotation-in-python-health-scoring-at-scale/final python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # then set CRAWLBASE_TOKEN
Le token appartient à l'environnement et non au code source, ce qui relève de l'hygiène ordinaire mais trace aussi la frontière de configuration sur laquelle s'appuie la section suivante.
La configuration comme contrat
La configuration est le premier contrat d'exécution. Le rotateur doit refuser de démarrer quand une dépendance requise manque, plutôt que de démarrer avec une route inutilisable et de le découvrir en pleine charge.
def _required(name: str) -> str: value = os.environ.get(name) if not value: raise RuntimeError(f"Missing required environment variable: {name}") return value @dataclass(frozen=True) class Config: crawlbase_token: str = field(default_factory=lambda: _required("CRAWLBASE_TOKEN")) smart_proxy_host: str = field(default_factory=lambda: os.environ.get("SMART_PROXY_HOST", "smartproxy.crawlbase.com")) smart_proxy_port: int = field(default_factory=lambda: int(os.environ.get("SMART_PROXY_PORT", "8012"))) success_decay: float = field(default_factory=lambda: float(os.environ.get("SUCCESS_DECAY", "0.3"))) latency_decay: float = field(default_factory=lambda: float(os.environ.get("LATENCY_DECAY", "0.3"))) breaker_threshold: int = field(default_factory=lambda: int(os.environ.get("BREAKER_THRESHOLD", "3"))) breaker_cooldown_s: float = field(default_factory=lambda: float(os.environ.get("BREAKER_COOLDOWN_S", "15")))
Notez ce que dit cette séparation. CRAWLBASE_TOKEN est un identifiant et il est requis. Tout le reste est de la politique : à quelle vitesse l'estimateur de succès réagit, à quelle vitesse celui de latence réagit, combien d'échecs consécutifs ouvrent le breaker, et combien de temps une route ouverte reste dehors. Geler la dataclass fait que le chemin de routage lit une configuration fixe au lieu d'aller chercher des variables d'environnement au moment de la requête.
La référence à battre
Une route, ici, c'est tout ce qui peut récupérer une URL et rapporter ce qui s'est passé. L'implémentation en fournit deux : une connexion directe et le Smart AI Proxy.
class CrawlbaseRoute(Route): name = "crawlbase-smart-proxy" def __init__(self, config: Config) -> None: proxy_url = ( f"http://{config.crawlbase_token}:@" f"{config.smart_proxy_host}:{config.smart_proxy_port}" ) self._client = httpx.Client(proxy=proxy_url, verify=False, timeout=config.request_timeout_s, follow_redirects=True)
Le token est passé comme nom d'utilisateur du proxy avec un mot de passe vide, c'est ainsi que le Smart AI Proxy authentifie. verify=False n'est pas un raccourci et mérite d'être compris plutôt que recopié : le proxy termine le TLS pour ajouter ses propres en-têtes, votre client se voit donc présenter un certificat Crawlbase et non celui de la cible, et une vérification stricte le rejetterait. C'est le comportement documenté du point d'entrée, pas un contournement de mauvaise configuration.
DirectRoute est la même interface sans proxy. Elle est souvent plus rapide face à une cible non protégée et se dégrade en premier quand des contrôles anti-bot apparaissent, ce qui en fait une source utile du signal d'échec que le modèle de santé est fait pour consommer.
La politique de référence ignore chacun de ces signaux, délibérément :
class NaiveRotator: def __init__(self, routes: list[Route]) -> None: self._routes = routes self._cycle = itertools.cycle(routes) def fetch(self, url: str) -> tuple[str, FetchResult]: route = next(self._cycle) return route.name, route.fetch(url)
C'est toute la politique. Elle est déterministe et parfaitement équitable, et l'équité est le problème : elle persiste après que les routes ont cessé d'être aussi bonnes les unes que les autres.
Noter la santé : deux moyennes et un breaker
Une moyenne mobile à pondération exponentielle convient ici parce qu'elle pondère les observations récentes et oublie graduellement les anciennes. Une route tombée il y a dix minutes ne doit pas être punie éternellement ; une route qui a commencé à tomber il y a trente secondes doit chuter vite. Le facteur de décroissance est le curseur entre les deux.
def observe(self, ok: bool, latency_s: float) -> None: self.samples += 1 outcome = 1.0 if ok else 0.0 self.success_ewma = self.success_decay * outcome + (1 - self.success_decay) * self.success_ewma if ok: self.latency_ewma_s = self.latency_decay * latency_s + (1 - self.latency_decay) * self.latency_ewma_s self.consecutive_failures = 0 if self.breaker_state is BreakerState.HALF_OPEN: self.breaker_state = BreakerState.CLOSED else: self.consecutive_failures += 1 if self.consecutive_failures >= self.breaker_threshold: self.breaker_state = BreakerState.OPEN self.opened_at = time.monotonic()
Deux choix y sont délibérés. La latence n'est mise à jour qu'en cas de succès, car la durée d'une requête en échec décrit l'échec et non la réactivité de la route quand elle marche ; les intégrer ferait passer pour rapide une route qui échoue vite. Et la stabilité est confiée au breaker plutôt qu'à la moyenne, parce que des échecs consécutifs sont une autre nature de preuve qu'une moyenne qui dérive, et méritent une réponse plus tranchée.
Les deux estimateurs s'effondrent ensuite en un seul nombre :
def value(self) -> float: if self.breaker_state is BreakerState.OPEN: return 0.0 latency_term = 1.0 / (1.0 + self.latency_ewma_s) return self.success_ewma * latency_term
La multiplication plutôt que l'addition est la décision de conception ici, et elle encode une conjonction : une route doit être fiable et réactive pour bien scorer. Additionnez les termes et une route rapide qui échoue la plupart du temps accumule encore un score respectable grâce à sa moitié latence. Multipliez-les et un taux de succès proche de zéro tire toute la valeur vers zéro, quelle que soit la rapidité des échecs. Un breaker ouvert court-circuite à exactement 0.0, ce qui rend le retrait explicite plutôt qu'émergent.
La boucle, une requête à la fois
Chaque requête suit les mêmes quatre étapes : trouver les routes éligibles, en choisir une selon la santé, exécuter, réinjecter le résultat.
observe est la seule étape qui change le comportement futur.def fetch(self, url: str) -> tuple[str, FetchResult]: candidates = self._eligible() or self._scored chosen = self._select(candidates) result = chosen.route.fetch(url) chosen.health.observe(result.ok, result.latency_s) return chosen.route.name, result
La sélection pondère chaque route éligible par sa santé élevée à un exposant, gamma. À zéro le choix est uniforme ; à mesure qu'il grandit, le trafic se concentre sur les routes les mieux notées. Gardez-le modeste. Un exposant élevé produit un rotateur qui s'engage à fond sur la route qui a semblé bonne en premier, puis n'a plus aucun moyen de découvrir qu'une route rétrogradée s'est rétablie, puisqu'il ne lui envoie plus rien.
Le repli de la première ligne compte plus qu'il n'en a l'air : quand le breaker s'est ouvert sur tout, _eligible() est vide et le rotateur retombe sur l'ensemble noté complet au lieu de lever une exception. Un pool entièrement malade doit quand même tenter l'option la moins mauvaise.
Ce que le benchmark montre réellement
python src/main.py benchmark 20 Policy comparison: policy requests success rate mean latency round-robin 20 100.0% 1.589s health-weighted 20 100.0% 0.289s
Lisez-le attentivement, car le chiffre en une est la partie la moins intéressante. Face à une cible non protégée comme example.com les deux routes réussissent, les taux de succès sont donc identiques et tout l'écart atterrit dans la latence. Le round-robin continue d'envoyer la moitié de son trafic par la route la plus lente, puisque c'est ce que veut dire l'équité. La politique pondérée par la santé le remarque et arrête.
Les latences absolues varient selon les exécutions et les cibles et ne constituent pas l'affirmation. L'affirmation porte sur le comportement : une politique réagit aux preuves, l'autre en est incapable. Pointez le même benchmark vers une cible protégée et le même mécanisme s'exprime dans le taux de succès : l'EWMA de succès de la route directe chute, ses échecs consécutifs déclenchent le breaker, et le trafic se déplace pendant que le round-robin continue d'alimenter une route qui les refuse.
Un point d'entrée devant des IP résidentielles rotatives, avec la résolution anti-bot gérée en amont, pour que votre rotateur note une route au lieu de gérer une flotte d'adresses. Pointez-y un client HTTP et laissez votre politique de routage là où elle doit être. Commencez gratuitement avec jusqu'à 5 000 requêtes, sans carte.
Passer en production
Le benchmark est mono-processus et synchrone parce que cela rend la boucle de contrôle lisible. Quatre choses changent quand ce n'est plus le cas.
L'état de santé doit être partagé. En mémoire, chaque worker garde son opinion privée sur le pool, donc un processus peut considérer une route cassée pendant qu'un autre continue joyeusement de l'utiliser, et le breaker ne s'ouvre nulle part de façon cohérente. Déplacer l'EWMA de succès, l'EWMA de latence, l'état du breaker et les compteurs d'échecs dans quelque chose comme Redis, sous une clé cohérente par route, est ce qui rend le signal collectif plutôt que folklorique par processus.
Les mises à jour doivent être sûres en concurrence. Les charges réelles observent les résultats en parallèle, et chaque champ dans observe est un read-modify-write. Sans synchronisation ni opérations atomiques, deux échecs simultanés peuvent lire le même compteur et réécrire le même incrément, si bien qu'un seuil de trois devient discrètement un seuil de cinq.
Les facteurs de décroissance doivent être réglés sur votre trafic. 0.3 est un point de départ défendable, pas une réponse. Des valeurs plus hautes courent après les observations récentes et réagissent plus vite, au prix d'une surréaction à un soubresaut passager. Des valeurs plus basses sont plus stables et remarquent plus tard un vrai changement. L'erreur que vous préférez dépend du bruit de vos cibles.
La santé n'est peut-être pas votre seul objectif. Les routes diffèrent en coût autant qu'en qualité, et la route la plus rapide n'est pas automatiquement celle que vous voulez sur chaque requête. Étendre le scalaire avec un terme de coût permet à la politique d'arbitrer fiabilité et latence contre dépense, au lieu d'optimiser une dimension et d'être surpris par la facture.
Une limite se situe hors du modèle : pointez ceci vers des cibles auxquelles vous avez le droit d'accéder, et respectez leurs conditions, leurs attentes de débit et leurs contraintes. Le benchmark utilise example.com précisément parce que c'est une cible contrôlée qui n'exige rien de tout cela.
À retenir
Un rotateur devient utile dès l'instant où la sélection est guidée par le comportement observé plutôt que par la position dans une liste. Trois mécanismes font le travail : une EWMA sur le succès pour la fiabilité récente, une EWMA sur la latence pour la réactivité, et un circuit breaker pour les défaillances brutales qu'une moyenne lisse. Multiplier les deux premières garde une route honnête sur les deux axes ; le breaker prend en charge le cas où progressif est la mauvaise vitesse.
Le round-robin reste dans le tableau comme le témoin qui rend l'amélioration mesurable. Et le Smart AI Proxy s'insère comme une route plutôt que comme une responsabilité, absorbant la rotation d'IP et la résolution anti-bot pour que la politique au-dessus reste une décision de routage.
Sélectionner, exécuter, observer, mettre à jour, sélectionner à nouveau. Tout le reste de cet article est un détail sur la qualité d'exécution de chaque étape.
Questions fréquentes (FAQ)
Pourquoi noter les routes moi-même si le Smart AI Proxy fait déjà tourner les IP ?
Ils opèrent à des niveaux différents. Le Smart AI Proxy fait tourner les IP à l'intérieur de son propre pool, votre application n'a donc pas à modéliser des adresses individuelles. Votre rotateur note des routes: une connexion directe contre une connexion proxifiée, un point d'entrée régional contre un autre, un chemin économique contre un chemin coûteux. C'est une décision que seule votre application peut prendre, parce qu'elle seule sait à quoi sert le trafic. Le modèle de cet article est délibérément général pour s'appliquer à l'ensemble de routes que vous exploitez réellement.
Quel facteur de décroissance utiliser ?
Commencez à 0.3 pour les deux. Chaque nouvelle observation apporte 30% de l'estimation mise à jour et l'estimation précédente porte les 70% restants. Augmentez-le quand vos cibles changent vite de comportement et que la politique doit s'en apercevoir plus tôt ; baissez-le quand les mesures sont bruitées et qu'une seule réponse lente ne doit pas déplacer le trafic.
Quand le circuit breaker aide-t-il plus que l'EWMA de succès seule ?
Quand la panne arrive d'un coup. L'EWMA est un lisseur par construction, donc une route qui meurt franchement met encore plusieurs observations à descendre assez bas pour compter, et chacune de ces observations est une requête gâchée. Le breaker s'appuie plutôt sur les échecs consécutifs et retire la route immédiatement, puis propose une sonde half-open contrôlée après le refroidissement au lieu d'attendre que la moyenne remonte. Les deux sont complémentaires : la moyenne traite la dégradation, le breaker l'effondrement.
Ai-je besoin du JavaScript token pour cela ?
Non. Le Normal token suffit pour la route Smart AI Proxy utilisée ici. Le JavaScript token existe pour les cibles qui ont besoin d'un rendu avant qu'il y ait le moindre contenu à renvoyer, ce qui est une question sur la cible et non sur le routage. La logique de rotation est identique dans les deux cas.
Le même modèle peut-il router plus de deux options ?
Oui, et il est même plus utile ainsi. Ni le modèle de santé ni la politique de sélection ne supposent deux routes : les deux travaillent sur une liste. Deux routes rendent simplement le benchmark facile à lire. Ajouter des points d'entrée régionaux ou un second fournisseur revient à allonger la liste des routes, et la sélection pondérée répartit sur tout ce qui s'y trouve.
Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.
Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.
