curl_cffi против requests: что исправляет и что не может
Замена requests на curl_cffi превращает множество 403 в 200. Это также не помогает при заблокированном IP. Вот граница между ними и пятнадцатиминутный тест, который покажет, на какой стороне ваша проблема.
Вопрос curl_cffi против requests обычно возникает в разгар инцидента: скрейпер, который работал месяцами, начинает возвращать 403 на первом вызове, кто-то на Reddit говорит заменить клиент, и это работает. Это реальный эффект с реальным объяснением — но то, как это повторяется ("просто используйте curl_cffi"), скрывает как то, что происходит, так и где это перестает помогать. requests не медленный и не плохо написан. У него есть ровно один недостаток в контексте скрейпинга, он значительный, и он не имеет ничего общего с API, который вы вводите. Здесь клиенты действительно различаются, что меняется при переключении, и одна вещь, которую имитация никогда не исправит, независимо от того, какую библиотеку вы выберете.
Единственное различие, которое имеет значение
Обе библиотеки отправляют одинаковые заголовки. Различие в одном уровне ниже, в TLS рукопожатии, которое открывает соединение до того, как передается первый байт HTTP. requests использует urllib3 и OpenSSL, которые рекламируют список шифров, набор расширений и порядок, принадлежащие Python и ничему другому. curl_cffi — это привязка к исправленной версии curl, которая воспроизводит ClientHello браузера байт за байтом, вместе с его HTTP/2 SETTINGS кадром — так что его JA3, JA3N и Akamai хэши совпадают с настоящим Chrome, а не с библиотекой скриптов. Поставщики антиботов ведут базы данных этих подписей; несоответствие между заголовком User-Agent Chrome и рукопожатием Python — это противоречие, которое невозможно объяснить. Мы разобрали механику в JA3 и JA4 отпечатках, и тот же эффект объясняет, почему curl получает 403, где ваш браузер получает 200 на идентичном URL.
Все остальное в сравнении следует из реализации. Поскольку curl_cffi оборачивает libcurl, он наследует HTTP/2, HTTP/3, вебсокеты и asyncio, ни одно из которых requests никогда не поддерживал. Поскольку requests написан на чистом Python, он устанавливается на что угодно и имеет десятилетнюю экосистему. Оба утверждения верны одновременно, и какое из них доминирует, зависит полностью от вашей цели.
Воспроизводимый тест, который вы можете провести за пять минут
Не доверяйте таблице пропусков никого, включая нашу. Различие в отпечатках можно наблюдать напрямую: направьте оба клиента на TLS-echo endpoint и сравните хэши, которые они возвращают. Если две строки совпадают, ваша сборка ничего не имитирует.
# 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.
С версии v0.15 есть однострочная версия той же проверки: curl-cffi get tls.browserleaks.com/json --impersonate chrome. Запустите это перед тем, как отлаживать что-либо еще — это отделяет "моя имитация неправильно настроена" от "моя имитация в порядке, и что-то еще меня блокирует", что совершенно разные ситуации.

Что curl_cffi не исправляет: репутация IP
Вот часть, которую упускают из виду в совете "замените библиотеку". TLS отпечаток отвечает на вопрос "какое это программное обеспечение?". Он ничего не говорит о "откуда это?" — и на этот второй вопрос отвечает отдельный запрос к вашему выходному IP: какой ASN владеет им, является ли это хостинг-провайдером или потребительским ISP, появлялся ли он в списках злоупотреблений, сколько других сессий было с этого адреса на этот сайт за последний час. Безупречное рукопожатие Chrome, прибывающее из облачного VM в диапазоне датацентра, это браузер Chrome, который, по-видимому, установлен в стойке сервера. Это не более убедительно, чем python-requests. В некоторых случаях это даже менее убедительно, потому что противоречие более явное.
FAQ проекта ставит качество IP на первое место в списке факторов, перед частотой запросов и отпечатками JavaScript, объясняя, почему одной имитации может быть недостаточно. Этот порядок не случаен: репутация — это самый дешевый сигнал для оценки защитником и самый сложный для подделки атакующим, потому что, в отличие от заголовка или списка шифров, вы не можете сгенерировать его локально. Если ваш скрейпер на базе requests уже работал через пул датацентра и блокировался, переход на curl_cffi в том же пуле изменяет одну из двух провальных проверок. Вы увидите частичное улучшение на мягких целях и никакого улучшения на жестких — это именно тот запутанный результат, о котором сообщают люди.
Исправление для этой оси — это качество адреса, а не код: резидентные IP от реальных потребительских ISP, что дает вам более 90 миллионов адресов в более чем 200 странах. Если вы хотите проверить, как выглядит ваш текущий выходной IP, прежде чем что-то менять, наш бесплатный проверщик качества IP сообщает ASN и классификацию, которую увидит цель.
2x2, который покажет, какая ось сломана
Вместо того чтобы гадать, протестируйте обе переменные независимо на вашей реальной цели. Четыре запроса, четыре строки вывода, и результат назовет вашу проблему:
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))
Читайте четыре результата как таблицу истинности:
- Только строки датацентра не проходят — это репутация IP. Клиент не имеет значения; купите лучшие выходы.
- Только строки requests не проходят — это отпечаток TLS. Переключите клиент и оставьте существующий пул.
- Только последняя строка проходит — обе проверки активны. Вам нужны имитация и чистые IP вместе; это обычный случай на серьезных целях.
- Все четыре не проходят — вы за пределами того, что может сделать любой HTTP клиент. Это означает JavaScript вызов, токен, который вы не генерируете, или блокировку на уровне учетной записи. Используйте реальный браузер или управляемый Scraper API.
- Все четыре проходят — у вас никогда не было проблемы с отпечатками. Не добавляйте зависимость.
Запустите это несколько десятков раз, а не один раз. Оба уровня блокировки вероятностные, и один 200 почти ничего не говорит.
Тестируйте строку резидентных IP с реальными домашними IP

Когда дополнительная зависимость не стоит того
Откровенность дешевле, чем переписывание. Оставайтесь на requests, когда:
- Вы вызываете API, который вам разрешено вызывать. Документированные endpoints с вашим собственным ключом не отпечатывают вас. Добавление имитации здесь — это карго-культ.
- Ваша цель развертывания неудобна. curl_cffi поставляется с компилированными wheels и требует Python 3.10 или новее с версии v0.14. requests работает практически на всем, включая старые образы и ограниченные встроенные среды.
- Вы зависите от экосистемы requests. Пользовательские адаптеры, requests-cache, requests-oauthlib и подобные все подключаются к транспортному уровню, который curl_cffi намеренно не раскрывает.
- Вам нужны повторные попытки по коду состояния из коробки.
Retryиз urllib3 сstatus_forcelistповторяет попытки на 429 и 503; параметрretrycurl_cffi повторяет только при транспортных исключениях. - Ваши блокировки поведенческие. Ограничения скорости, блокировки учетных записей и квоты на сессию не зависят от рукопожатия.
И есть средний путь, который большинство людей упускает: вам не нужно отказываться от requests, чтобы получить рукопожатие. Поддерживающие указывают на curl-adapter, который монтирует curl_cffi как транспортный адаптер для requests, и httpx-curl-cffi на PyPI, который делает то же самое для httpx. Вы сохраняете свой существующий код и экосистему, и только байты на проводе меняются.
Подводные камни миграции, которые стоит знать заранее
API достаточно близок, чтобы большинство скриптов работали после изменения импорта, но страница совместимости перечисляет реальные различия, и стоит их прочитать перед крупным портом. Тела ответов на перенаправления не сохраняются в Response.history. Cookies с пустыми доменами могут теряться при перенаправлениях. Потоковые объекты ответов не могут быть сериализованы, хотя обычные ответы могут. API файлов немного отличается. И вообще нет транспортов или адаптеров, потому что библиотека намеренно приварена к libcurl-impersonate. Конфигурация прокси также отличается небольшим образом, который сбивает людей с толку:
# 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
Полная поверхность прокси — ключи словаря, proxy_auth, ротация по запросу, async и префикс https://, который вызывает бесполезную ошибку WRONG_VERSION_NUMBER — покрыта шаг за шагом в нашем руководстве по прокси curl_cffi. Если вы остаетесь на месте, эквивалентная ссылка для другой стороны — это наше руководство по прокси Python Requests.
Часто задаваемые вопросы
curl_cffi быстрее requests?
Да, и бенчмарки проекта ставят его наравне с aiohttp и pycurl, а не с requests. Прирост идет от того, что libcurl выполняет работу на C плюс мультиплексирование HTTP/2, а не от хитрого Python. Для нескольких последовательных вызовов разница незаметна; при высокой параллельности, особенно с async, она значительна.
curl_cffi безопасен в использовании?
Он лицензирован по MIT, широко используется и поставляется с предварительно скомпилированными wheels, так что нет этапа сборки для аудита. Стоит обратить внимание на одно предупреждение: в версии v0.15.0 есть рекомендация по поводу SSRF на основе перенаправлений. Если вы получаете URL от других людей, установите allow_redirects="safe" или отключите перенаправления. Имитация браузера — это техническая мера, а не разрешение игнорировать условия сайта.
curl_cffi обходит Cloudflare?
Он убирает отпечатки TLS и HTTP/2, что устраняет базовые уровни защиты. Он не может выполнить JavaScript вызов, решить Turnstile или исправить помеченный выходной IP. Поддерживающие говорят об этом в своем FAQ и рекомендуют лучший пул прокси плюс автоматизацию браузера для более высоких уровней.
curl_cffi против httpx или tls_client — что мне использовать?
httpx дает вам HTTP/2 и async, но без имитации отпечатков, так что он находится между requests и curl_cffi по скрытности. tls_client также подделывает профили TLS и имеет схожие бенчмарки; curl_cffi имеет более крупное сообщество и добавляет HTTP/3 и вебсокеты. Если httpx уже в вашем стеке, транспорт httpx-curl-cffi дает вам имитацию без переписывания.
Краткая версия: переключайтесь на curl_cffi, когда ваша цель читает рукопожатия, оставайтесь на requests, когда этого не происходит, и никогда не ожидайте, что любой из выборов очистит IP датацентра. Клиенты различаются по одной оси, прокси по другой, и заблокированные скрейперы почти всегда история о том и другом. Запустите четыре проверки, прочитайте таблицу истинности и исправьте ось, на которую указывает данные, а не ту, о которой кричал интернет.