Обход TLS/JA3-фингерпринтинга с помощью curl_cffi
Опубликовано: 23 июля 2026 · 7 мин чтения
Кратко: Если ваш скрапер блокируют даже с чистого резидентного IP, цель почти наверняка снимает отпечаток вашего TLS-рукопожатия (JA3/JA4) и фреймов HTTP/2. Python-библиотеки requests и httpx выдают отпечаток, которого нет ни у одного настоящего браузера, поэтому вас помечают ещё до загрузки контента. curl_cffi имитирует TLS-сигнатуру настоящего браузера. Используйте его вместе с резидентным прокси — одно решает проблему репутации IP, другое — отпечатка, а серьёзные цели проверяют оба.
Блокировка, не имеющая отношения к вашему IP
Вы подставляете свежий резидентный IP, задаёте настоящий User-Agent Chrome, копируете заголовки из вкладки сети браузера — а цель всё равно отдаёт 403 или бесконечную капчу. В запросе на первый взгляд всё в порядке. Проблема в том, что нельзя задать на уровне приложения: в самом TLS-рукопожатии.
Когда ваш клиент открывает HTTPS-соединение, он отправляет ClientHello со списком поддерживаемых наборов шифров, расширений и кривых в определённом порядке. Этот шаблон хешируется в отпечаток JA3 (или более новый JA4). У настоящего Chrome одна сигнатура; у Python requests, использующего системный OpenSSL, — совершенно другая. Поставщики антибот-решений ведут список того, какие отпечатки принадлежат настоящим браузерам, а какие — скриптовым библиотекам, и проверяют его во время рукопожатия — ещё до оценки ваших заголовков или IP.
Так что у вас может быть идеальный резидентный IP и идеальные заголовки, а вас всё равно пометят, потому что ваш TLS-отпечаток говорит «Python».
Решение: имитируйте настоящий браузер с curl_cffi
curl_cffi — это привязка Python к curl-impersonate, сборке curl, пропатченной для точного воспроизведения TLS- и HTTP/2-отпечатков настоящих браузеров. Его API намеренно повторяет requests, так что переход в основном сводится к изменению одной строки.
pip install curl_cffi
Базовый запрос, имитирующий Chrome:
from curl_cffi import requests
r = requests.get("https://example.com", impersonate="chrome")
print(r.status_code, r.text[:200])
Аргумент impersonate — это вся суть: он указывает curl_cffi отправлять порядок шифров/расширений Chrome, чтобы хеш JA3 совпадал с настоящим браузером. Можно закрепить конкретную версию (chrome124, chrome131, safari17_0, firefox133) или передать chrome для актуального значения по умолчанию.
Добавление резидентного прокси
Отпечаток теперь верный, но серьёзная цель всё равно следит за выходным IP. Направьте запрос через резидентный прокси с тем же аргументом proxies=, который вы уже знаете по requests:
from curl_cffi import requests
proxies = {"https": "http://USER:PASS@gw.roamproxy.com:41080"}
r = requests.get(
"https://example.com",
impersonate="chrome124",
proxies=proxies,
)
print(r.status_code)
Теперь рукопожатие выглядит как у Chrome и соединение выходит с настоящего IP домашнего интернет-провайдера. Именно эту комбинацию и проверяют серьёзные цели. curl_cffi также принимает SOCKS5-прокси, если вам так удобнее — используйте socks5://USER:PASS@gw.roamproxy.com:41080 в качестве значения.
Избегайте спецсимволов в пароле прокси. Значение прокси разбирается как URL, поэтому #, @, / или : в пароле сломают разбор. Закодируйте их в процентную форму (# становится %23) или сгенерируйте пароль без них.
Сессия для многих запросов
Для реального обхода переиспользуйте сессию, чтобы соединение и отпечаток задавались один раз:
from curl_cffi import requests
session = requests.Session(impersonate="chrome124")
session.proxies = {"https": "http://USER:PASS@gw.roamproxy.com:41080"}
for url in urls:
r = session.get(url)
...
Чтобы распределить большую задачу по многим выходным IP, используйте сессию с ротацией, чтобы каждый запрос выходил с другого резидентного IP; чтобы удержать один IP на протяжении процесса входа, используйте sticky-сессию. О том, что выбрать и как строка сессии это контролирует, рассказано в статье что такое прокси с ротацией.
Проверьте, что отпечаток действительно изменился
Проверьте свой JA3 до и после. Эндпоинт для эхо-проверки отпечатка возвращает хеш, который видит сервер:
from curl_cffi import requests
print(requests.get("https://tls.browserleaks.com/json", impersonate="chrome").json()["ja3_hash"])
Выполните тот же вызов с обычным requests, и вы получите другой хеш — именно эта разница и была причиной блокировки со стороны цели.
Частые ошибки
- Имитация устаревшего браузера. Отпечаток
chrome99в 2026 году сам по себе является признаком подозрительности. Закрепляйте актуальную версию и обновляйте её вместе с браузерами. - Противоречие самому себе. Не задавайте заголовок User-Agent от Safari, имитируя Chrome — заголовок и отпечаток должны совпадать.
- Отпечаток без IP или IP без отпечатка. Исправление лишь одного из двух оставляет вторую проверку, которая вас поймает. На защищённых целях нужны оба.
Более широкую картину того, почему запросы блокируются и что ещё проверить, см. в статье как избежать блокировок при веб-скрапинге.
Часто задаваемые вопросы
Почему меня блокируют даже со свежим резидентным прокси?
Потому что IP — лишь один из сигналов. Такие антибот-системы, как Cloudflare, DataDome и Akamai, снимают отпечаток TLS ClientHello (JA3/JA4) и порядок фреймов HTTP/2 в момент открытия соединения — ещё до того, как они увидят репутацию вашего IP или ваши заголовки. Стандартный стек Python выдаёт отпечаток, который не производит ни один настоящий браузер, поэтому вас распознают как автоматизацию независимо от того, насколько чистый выходной IP. Резидентный прокси и точный отпечаток браузера — это два разных решения для двух разных проверок; серьёзные цели проводят обе.
Что именно такое отпечаток JA3?
JA3 — это хеш определённых полей TLS ClientHello: набора шифров, которые предлагает ваш клиент, расширений, эллиптических кривых, в точном порядке их отправки. Разные HTTP-клиенты упорядочивают их по-разному, поэтому хеш фактически определяет программное обеспечение, отправившее запрос. У настоящего Chrome один JA3; у Python requests (через OpenSSL) — совершенно другой. JA4 — более новый и подробный преемник. curl_cffi воспроизводит порядок настоящего браузера, поэтому хеш совпадает с Chrome или Safari, а не с «Python».
Достаточно ли одного curl_cffi без прокси?
Для слабо защищённого сайта — иногда да. Для чего-то серьёзного — нет. Имитация отпечатка проведёт вас мимо проверки TLS/HTTP2, но если вы отправляете сотни запросов с одного дата-центрового IP, вас всё равно ограничат по частоте или заблокируют по репутации IP. Надёжная связка — это curl_cffi для отпечатка плюс вращающийся резидентный прокси для IP — почему тип IP важен, см. в статье резидентные и дата-центровые прокси.
requests, httpx или curl_cffi — что использовать?
requests и httpx отлично подходят для API и сайтов, не снимающих отпечаток. В тот момент, когда цель находится за Cloudflare/DataDome/Akamai и блокирует вас несмотря на правильные заголовки и хороший IP, переходите на curl_cffi — его API почти полностью повторяет requests, так что миграция обычно сводится к замене строки импорта и добавлению аргумента impersonate=.
curl_cffi исправляет отпечаток; чистый выходной IP исправляет репутацию. Roam даёт вращающиеся резидентные IP по $2/ГБ и статические резидентные по $4/IP в месяц через HTTP и SOCKS5, так что он без проблем встаёт в аргумент proxies= выше. Создайте аккаунт и получите 300 МБ бесплатного пробного трафика, чтобы протестировать связку на своей цели.