curl_cffi vs requests : Ce qu'il corrige et ce qu'il ne peut pas

Remplacer requests par curl_cffi transforme beaucoup de 403 en 200. Cela ne fait rien du tout pour une IP brûlée. Voici la ligne entre les deux, et un test de quinze minutes qui vous indique de quel côté se trouve votre problème.

La question curl_cffi vs requests arrive généralement en plein incident : un scraper qui fonctionnait depuis des mois commence à renvoyer 403 dès le premier appel, quelqu'un sur Reddit dit de changer de client, et ça fonctionne. C'est un effet réel avec une explication réelle — mais la façon dont cela est répété ("utilisez simplement curl_cffi") cache à la fois ce qui se passe et où cela cesse d'aider. requests n'est pas lent ou mal écrit. Il a exactement un inconvénient dans un contexte de scraping, il est important, et cela n'a rien à voir avec l'API que vous tapez. Voici où les deux clients diffèrent réellement, ce qui change lorsque vous changez, et la seule chose que l'usurpation ne corrigera jamais, peu importe la bibliothèque que vous choisissez.

La seule différence qui compte

Les deux bibliothèques envoient les mêmes en-têtes. La différence se situe un niveau en dessous, dans la poignée de main TLS qui ouvre la connexion avant qu'un seul octet HTTP ne circule. requests repose sur urllib3 et OpenSSL, qui annoncent une liste de chiffrement, un ensemble d'extensions et un ordre qui appartiennent à Python et à rien d'autre. curl_cffi est un lien vers un fork modifié de curl qui reproduit le ClientHello d'un navigateur octet par octet, ainsi que son cadre HTTP/2 SETTINGS — de sorte que ses empreintes JA3, JA3N et Akamai correspondent à un vrai Chrome plutôt qu'à une bibliothèque de script. Les fournisseurs anti-bot conservent des bases de données de ces signatures ; une discordance entre un en-tête User-Agent de Chrome et une poignée de main Python est une contradiction dont vous ne pouvez pas vous sortir. Nous avons décomposé la mécanique dans l'empreinte JA3 et JA4, et le même effet explique pourquoi curl obtient un 403 là où votre navigateur obtient un 200 sur une URL identique.

Tout le reste de la comparaison découle de l'implémentation. Parce que curl_cffi enveloppe libcurl, il hérite de HTTP/2, HTTP/3, des websockets et d'asyncio, qu'aucun requests n'a jamais pris en charge. Parce que requests est en pur Python, il s'installe sur n'importe quoi et a une décennie d'écosystème derrière lui. Les deux affirmations sont vraies à la fois, et laquelle domine dépend entièrement de votre cible.

Un test reproductible que vous pouvez réaliser en cinq minutes

Ne prenez pas pour argent comptant le tableau des taux de réussite de quiconque, y compris le nôtre. La différence d'empreinte est directement observable : pointez les deux clients vers un point de terminaison TLS-echo et comparez les empreintes qu'ils renvoient. Si les deux lignes correspondent, votre build n'usurpe rien.

# pip install requests curl_cffi
import requests
import curl_cffi

URL = "https://tls.browserleaks.com/json"

a = requests.get(URL, timeout=30).json()
b = curl_cffi.get(URL, impersonate="chrome", timeout=30).json()

print("requests   ja3n:", a["ja3n_hash"], "| akamai:", a.get("akamai_hash", "-"))
print("curl_cffi  ja3n:", b["ja3n_hash"], "| akamai:", b.get("akamai_hash", "-"))

# Two different ja3n hashes = impersonation is working. The curl_cffi README
# documents aa56c057ad164ec4fdcb7a5a283be9fc as a Chrome-matched ja3n value.
# The akamai (HTTP/2) fingerprint is usually empty for requests: it is
# HTTP/1.1 only, so there is no SETTINGS frame to fingerprint in the first place.

Depuis la version v0.15, il existe une version en une ligne de la même vérification : curl-cffi get tls.browserleaks.com/json --impersonate chrome. Exécutez-la avant de déboguer quoi que ce soit d'autre — cela sépare "mon usurpation est mal configurée" de "mon usurpation est correcte et quelque chose d'autre me bloque", qui sont deux après-midi complètement différents.

Comparaison côte à côte de requests et curl_cffi sur le support des protocoles, la concurrence, l'empreinte et la portabilité
requests l'emporte sur la portabilité et l'écosystème ; curl_cffi l'emporte sur les protocoles et les empreintes. Aucun ne l'emporte sur la qualité IP — ce n'est pas une fonctionnalité du client.

Ce que curl_cffi ne corrige pas : la réputation IP

Voici la partie que le conseil de changer de bibliothèque omet. Une empreinte TLS répond à la question "quel logiciel est-ce ?". Elle ne dit rien sur "d'où cela vient-il ?" — et cette deuxième question est répondue par une recherche séparée contre votre IP de sortie : quel ASN la possède, est-ce un fournisseur d'hébergement ou un FAI consommateur, est-elle apparue dans des flux d'abus, combien d'autres sessions ont accédé à ce site depuis la même adresse dans la dernière heure. Une poignée de main Chrome impeccable arrivant d'une VM cloud dans une plage de datacenter est un navigateur Chrome qui a apparemment été installé dans une baie de serveurs. Ce n'est pas plus convaincant que python-requests. Dans certains cas, c'est moins, car la contradiction est plus nette.

La FAQ du projet met la qualité IP en premier dans sa liste de facteurs, devant le taux de requêtes et les empreintes JavaScript, lorsqu'elle explique pourquoi l'usurpation seule peut ne pas suffire. Cet ordre n'est pas un accident : la réputation est le signal le moins cher à évaluer pour un défenseur et le plus difficile à falsifier pour un attaquant, car contrairement à un en-tête ou une liste de chiffrement, vous ne pouvez pas la générer localement. Si votre scraper basé sur requests fonctionnait déjà à travers une piscine de datacenter et était bloqué, passer à curl_cffi sur la même piscine change l'un des deux contrôles défaillants. Vous verrez une amélioration partielle sur des cibles faciles et aucune amélioration du tout sur des cibles difficiles — ce qui est exactement le résultat confus que les gens rapportent.

La solution pour cet axe est la qualité de l'adresse, pas le code : des IPs résidentielles provenant de véritables allocations de FAI consommateurs, ce que plus de 90 millions d'adresses dans plus de 200 pays vous offrent. Si vous souhaitez vérifier à quoi ressemble votre sortie actuelle avant de changer quoi que ce soit, notre vérificateur de qualité IP gratuit rapporte l'ASN et la classification qu'une cible verrait.

Le 2x2 qui vous indique quel axe est cassé

Plutôt que de deviner, testez les deux variables indépendamment contre votre véritable cible. Quatre requêtes, quatre lignes de sortie, et le résultat nomme votre problème :

import requests
import curl_cffi

TARGET = "https://your-target.example/api/items"
DC  = "http://USER:PASS@your-datacenter-gateway:PORT"
RES = "http://USER:PASS@gate.quantumproxies.io:PORT"

def probe(label, fn):
    try:
        print(f"{label:26} -> {fn().status_code}")
    except Exception as e:
        print(f"{label:26} -> {type(e).__name__}")

probe("requests  + datacenter",
      lambda: requests.get(TARGET, proxies={"https": DC}, timeout=30))
probe("curl_cffi + datacenter",
      lambda: curl_cffi.get(TARGET, proxy=DC, impersonate="chrome", timeout=30))
probe("requests  + residential",
      lambda: requests.get(TARGET, proxies={"https": RES}, timeout=30))
probe("curl_cffi + residential",
      lambda: curl_cffi.get(TARGET, proxy=RES, impersonate="chrome", timeout=30))

Lisez les quatre résultats comme une table de vérité :

Exécutez-le quelques dizaines de fois plutôt qu'une seule. Les deux couches de blocage sont probabilistes, et un seul 200 ne vous dit presque rien.

Testez la ligne résidentielle avec de véritables IPs domestiques

Diagramme montrant une poignée de main TLS correspondant à un navigateur depuis une IP de datacenter étant bloquée tandis que la même poignée de main depuis une IP résidentielle passe
L'usurpation et la réputation IP sont des portes séparées. Corriger l'une et laisser l'autre est la raison pour laquelle 'j'ai changé pour curl_cffi et rien n'a changé' est un rapport si courant.

Quand la dépendance supplémentaire n'en vaut pas la peine

La franchise est moins coûteuse qu'une réécriture. Restez sur requests lorsque :

Et il y a un chemin intermédiaire que la plupart des gens manquent : vous n'avez pas à abandonner requests pour obtenir la poignée de main. Les mainteneurs pointent vers curl-adapter, qui monte curl_cffi comme un adaptateur de transport requests, et httpx-curl-cffi sur PyPI, qui fait de même pour httpx. Vous conservez votre code et votre écosystème existants, et seuls les octets sur le fil changent.

Les pièges de migration à connaître d'abord

L'API est suffisamment proche pour que la plupart des scripts fonctionnent après avoir changé l'importation, mais la page de compatibilité liste de réelles différences et il est utile de les lire avant un portage important. Les corps de réponse de redirection ne sont pas conservés dans Response.history. Les cookies avec des domaines vides peuvent être perdus lors des redirections. Les objets de réponse en streaming ne peuvent pas être sérialisés, bien que les réponses normales le puissent. L'API des fichiers diffère légèrement. Et il n'y a pas de transports ou d'adaptateurs du tout, car la bibliothèque est délibérément soudée à libcurl-impersonate. La configuration du proxy diffère également d'une petite manière qui piège les gens :

# requests: dict, and retries mounted on an adapter
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=Retry(total=4, status_forcelist=[429, 503])))
r = s.get(url, proxies={"https": "http://USER:PASS@gate.quantumproxies.io:PORT"}, timeout=30)

# curl_cffi: single proxy string preferred, retries are a session parameter
from curl_cffi import Session
from curl_cffi.requests import RetryStrategy

s = Session(
    impersonate="chrome",
    proxy="http://USER:PASS@gate.quantumproxies.io:PORT",
    retry=RetryStrategy(count=4, delay=1.0, backoff="exponential"),
    timeout=30,
)
r = s.get(url)  # note: retry fires on transport errors, not on 429/503

La surface complète du proxy — clés de dictionnaire, proxy_auth, rotation par requête, async et le préfixe https:// qui produit une erreur WRONG_VERSION_NUMBER peu utile — est couverte étape par étape dans notre guide du proxy curl_cffi. Si vous restez en place, la référence équivalente pour l'autre côté est notre guide du proxy Python Requests.

Questions fréquemment posées

curl_cffi est-il plus rapide que requests ?

Oui, et les benchmarks du projet le placent au même niveau qu'aiohttp et pycurl plutôt qu'avec requests. Le gain vient de libcurl qui fait le travail en C plus le multiplexage HTTP/2, pas d'un Python astucieux. Pour une poignée d'appels séquentiels, la différence est invisible ; à haute concurrence, surtout avec async, elle est substantielle.

curl_cffi est-il sûr à utiliser ?

Il est sous licence MIT, largement déployé et expédie des roues précompilées, donc il n'y a pas d'étape de construction à auditer. Une mise en garde vaut la peine d'être prise en compte : un avis de la version v0.15.0 couvre le SSRF basé sur la redirection. Si vous récupérez des URLs fournies par d'autres personnes, définissez allow_redirects="safe" ou désactivez les redirections. Usurper un navigateur est une mesure technique, pas une permission d'ignorer les conditions d'un site.

curl_cffi contourne-t-il Cloudflare ?

Il supprime l'indice d'empreinte TLS et HTTP/2, ce qui élimine les niveaux de protection de base. Il ne peut pas exécuter un défi JavaScript, résoudre Turnstile ou corriger une IP de sortie signalée. Les mainteneurs disent autant dans leur FAQ, et recommandent une meilleure piscine de proxy plus l'automatisation du navigateur pour les niveaux supérieurs.

curl_cffi vs httpx ou tls_client — lequel devrais-je utiliser ?

httpx vous offre HTTP/2 et async mais pas d'usurpation d'empreinte, donc il se situe entre requests et curl_cffi en termes de furtivité. tls_client imite également les profils TLS et a des performances similaires ; curl_cffi a la plus grande communauté et ajoute HTTP/3 et les websockets. Si httpx est déjà dans votre pile, le transport httpx-curl-cffi vous permet d'obtenir l'usurpation sans réécriture.

La version courte : passez à curl_cffi lorsque votre cible lit les poignées de main, restez sur requests lorsqu'elle ne le fait pas, et ne vous attendez jamais à ce que l'un ou l'autre choix blanchisse une IP de datacenter. Les clients diffèrent sur un axe, les proxies sur un autre, et les scrapers bloqués sont presque toujours une histoire des deux. Exécutez les quatre sondes, lisez la table de vérité, et corrigez l'axe que les données indiquent plutôt que celui dont internet a parlé.

Corrigez l'axe que l'usurpation ne peut pas atteindre