Как устранить блокировки по TLS-фингерпринту в Python Requests

Если ваши заголовки безупречны, а сайт всё равно блокирует вас, значит блокировка произошла ещё до того, как пришёл первый заголовок. TLS-фингерпринтинг определяет ваш HTTP-клиент по форме его TLS-рукопожатия, а у библиотеки Python requests рукопожатие такое, какое не производит ни один браузер на свете. Скопировав строку user agent, вы это не исправите. Исправить можно, только сделав само рукопожатие похожим на браузерное. В этой статье вы узнаете, как прочитать собственный фингерпринт, как его изменить и как отличить эту проблему от двух других, с которыми её часто путают.
Что понадобится
- Python 3.10 или новее. В примерах сначала используется библиотека requests, чтобы показать проблему, а затем curl_cffi, чтобы её исправить.
- Терминал и около десяти минут. Диагностический этап требует всего два запроса и не требует изменений в коде.
- Запущенный CapSkip нужен только для последнего раздела: либо в локальном режиме на loopback-адресе, либо в серверном режиме на машине, к которой могут обращаться ваши воркеры. Оба режима описаны в разделе Настройки подключения, так что выберите один из них перед началом.
Что на самом деле считывает TLS-фингерпринтинг
Каждое TLS-соединение начинается с сообщения ClientHello. В нём перечислены поддерживаемые вами версии TLS, предлагаемые наборы шифров, отправляемые расширения, принимаемые эллиптические кривые и порядок, в котором всё это перечислено. Ничего из этого не секрет, и ничего нельзя настроить для отдельного запроса. Это определяется той TLS-библиотекой, на основе которой собран ваш HTTP-клиент.
Если хешировать этот список, вы получите стабильный идентификатор клиентского ПО. JA3 стал первой широко используемой версией такого хеша. Сейчас используется JA4: он сортирует расширения ClientHello перед хешированием, поэтому один и тот же браузер не даёт разный фингерпринт при каждом соединении. Cloudflare описывает оба метода как способ определения TLS-клиентов по тому, как они инициируют соединения, и предоставляет это значение правилам файрвола, аналитике и Workers.
Вот почему это важнее любого заголовка, который вы задаёте. OpenSSL в том виде, в каком его использует Python, BoringSSL в том виде, в каком его использует Chrome, и NSS в том виде, в каком его использует Firefox, отправляют разные сообщения ClientHello. Поэтому запрос, который называет себя Chrome, а рукопожатие проводит как Python, выдаёт не тонкий признак, требующий хитрого распознавания, а два несовпадающих поля, которые правило для ботов может сравнить в одну строку.
Шаг 1: прочитайте свой фингерпринт, прежде чем что-либо менять
Не гадайте, в TLS-фингерпринтинге ли дело, ведь это можно измерить. Есть публичный эндпоинт, который возвращает хеш JA3, увиденный в вашем соединении, и сравнение двух запросов через него занимает меньше минуты.
# pip install requests
import requests
# The endpoint echoes back the handshake it received from you.
r = requests.get("https://tls.browserleaks.com/json", timeout=30)
seen = r.json()
print(seen["ja3_hash"]) # stable per TLS library, not per user agent
print(seen["ja3_text"]) # the raw cipher and extension listТеперь выполните тот же запрос ещё раз, указав в заголовках user agent браузера, и прочитайте хеш во второй раз. Он не изменится. Это весь урок в одном эксперименте: user agent живёт в заголовке, фингерпринт живёт в рукопожатии, и изменение первого никак не влияет на второе. Если вы меняли user agent по кругу, чтобы обойти блокировку, теперь понятно, почему это не сработало.
Шаг 2: проводите рукопожатие как настоящий браузер с помощью curl_cffi
Заставить стандартный TLS-стек Python выдавать ClientHello в стиле Chrome нельзя, потому что набор расширений и их порядок зашиты в сборку библиотеки, а не вынесены в настройки. Но можно использовать другую библиотеку. Пакет curl_cffi связывается со сборкой curl, которая воспроизводит рукопожатия конкретных браузеров, и предоставляет API в стиле requests, так что остальной код почти не меняется.
# One dependency, no browser and no driver involved. pip install curl_cffi --upgrade
Всю работу выполняет аргумент impersonate. Он определяет, сборку какого браузера должно копировать рукопожатие, и вы можете зафиксировать конкретную версию, если сайт стал придирчив к текущей.
# pip install curl_cffi
import curl_cffi
# Same call as before, but the handshake now matches Chrome.
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome")
print(r.json()["ja3_hash"]) # a different hash from the requests run above
# Pin a version when a site rejects the current default.
r = curl_cffi.get("https://example.com/", impersonate="chrome124")
print(r.status_code)Сравните два хеша рядом друг с другом. Если они отличаются, имперсонация работает, и это единственное подтверждение, которое имеет значение. Проект документирует поддерживаемые цели, которые включают Safari и сборку iOS Safari наряду с Chrome. Рукописные строки JA3 тоже принимаются, хотя поддерживаемый пресет почти всегда лучше, потому что пресеты обновляются вместе с браузерами.
Когда всё заработает, переходите на сессию. Вам нужны повторное использование соединения и хранилище cookie по тем же причинам, что и с requests.
# pip install curl_cffi
import curl_cffi
# One session, one handshake profile, cookies carried across calls.
s = curl_cffi.Session(impersonate="chrome")
s.get("https://example.com/login")
r = s.get("https://example.com/dashboard")
print(r.status_code, len(s.cookies))Шаг 3: сохраняйте согласованность остальной части клиента
Браузерное рукопожатие в паре с явно скриптовым запросом само по себе выглядит подозрительно несовместимым, а самая частая ошибка здесь: исправить один сигнал, оставив остальные бросающимися в глаза. Три параметра должны соответствовать выбранному вами профилю.
Ваш user agent должен указывать то же семейство браузера и примерно ту же версию, что и цель имперсонации. Заявлять о себе как о Firefox, проводя рукопожатие как Chrome, хуже, чем не заявлять вообще ничего, потому что это превращает слабый сигнал в уверенный.
Порядок заголовков имеет значение, как и поведение HTTP/2. Браузеры отправляют стабильный порядок заголовков, стабильный набор фреймов настроек HTTP/2 и стабильный порядок псевдозаголовков, и всё это считывается так же чётко, как и TLS-слой. Отправка заголовков в том порядке, в котором их случайно перебирает ваш словарь, сводит на нет всю проделанную работу. Это ещё один довод в пользу поддерживаемого пресета имперсонации, который сам берёт на себя HTTP/2-слой, вместо того чтобы оставлять это настройкам клиента по умолчанию.
Ваш IP-адрес должен соответствовать характеру отправляемого трафика. Адрес дата-центра, отправляющий похожие на браузерные запросы со скоростью машины, представляет собой отдельный сигнал, который никаким рукопожатием не исправить. Полный разбор вариантов есть в статье под названием ротация прокси при распознавании капчи, которая рассказывает, где ротация нужна внутри пула воркеров.
Ранжируем сигналы, чтобы исправлять их в правильном порядке
| Сигнал | Где считывается | Можно ли это изменить |
|---|---|---|
| TLS ClientHello, хешированный как JA3 или JA4 | До отправки любых HTTP-данных | Да, заменой TLS-библиотеки |
| Настройки HTTP/2 и порядок псевдозаголовков | Первые фреймы соединения | Да, и хороший пресет делает это за вас |
| Названия, значения и порядок заголовков | Сам запрос | Да, и это проще всего сделать неправильно |
| Репутация IP и тип адреса | Источник соединения | Только сменой места подключения |
| Сигналы браузерного окружения, такие как canvas и WebGL | Внутри реальной страницы, после выполнения JavaScript | Неприменимо, если вы не запускаете браузер |
| Частота запросов и характер навигации | На протяжении множества запросов | Да, снижением скорости и варьированием запрашиваемых путей |
Двигайтесь по этой таблице сверху вниз, а не поперёк. TLS-фингерпринтинг происходит раньше всего в соединении, поэтому это самое дешёвое место для защиты, чтобы принять решение, и именно туда должно попасть ваше исправление в первую очередь.
Чего всё это не исправит
Правильное рукопожатие не является входным билетом. Оно снимает одну из причин вам не доверять, а значит, сайт теперь оценивает остальной ваш трафик, вместо того чтобы отсеивать вас на пороге. Многие сайты всё равно покажут вам проверку после исправления фингерпринта, и это ожидаемый результат, а не признак того, что работа не удалась.
Это честная граница возможностей, и её стоит обозначить прямо. CapSkip не меняет ваш TLS-фингерпринт и не является слоем маскировки. Он решает капчу, которую вам показали, а это та часть проблемы, у которой в конце есть токен. Если между вами и телом ответа стоит виджет Turnstile или страница проверки, решатель формирует токен, а ваш существующий клиент его отправляет.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
# The challenge you were served, solved on your own machine.
result = solver.turnstile(
sitekey="YOUR_SITEKEY",
url="https://example.com/protected",
)
token = result["code"]
agent = result["userAgent"] # send this exact user agent with the tokenВозвращённый user agent играет не декоративную роль. Turnstile привязывает токен к идентичности браузера, который его получил, поэтому отправка полностью действительного токена с другим user agent надёжно приводит к его отклонению. Отправляйте оба значения вместе, и заявка будет принята. Об этом рассказывает страница Сервис распознавания Cloudflare Turnstile , которая описывает случай с виджетом и случай со страницей проверки отдельно, потому что им нужны разные входные данные.
Как запустить решатель на сервере
С фингерпринтом обычно приходится работать в пуле воркеров, а не в одном скрипте, а пул не использует общий loopback-адрес. Настройки подключения покрывают оба варианта:
| Режим | Прослушивает | Когда использовать |
|---|---|---|
| Локально | 127.0.0.1, только это устройство | Ваш скрейпер и решатель работают на одной машине |
| Сервер | Ваш сетевой адрес или публичный IP | Воркеры, VPS или облачная платформа обращаются через API |
Укажите в SDK хост машины с решателем, и больше ничего в коде менять не нужно, так что целый парк машин может использовать один экземпляр. Если вызывающие стороны находятся вне вашей сети, рекомендуется статический публичный IP. Подробности приведены в разделе Настройки подключения, а серверный режим по-прежнему работает на вашем оборудовании и без платы за использование: меняется лишь то, где выполняется решение капчи, а не то, кому принадлежит оборудование.
FAQ
Можно ли изменить TLS-фингерпринт самой библиотеки requests?
Не так, чтобы это принесло пользу. Вы можете изменить порядок наборов шифров через базовый SSL-контекст и тем самым сдвинуть хеш, но воспроизвести браузерный ClientHello таким способом не получится, потому что набор расширений и их порядок определяются сборкой библиотеки, а не настройками. В итоге вы получите фингерпринт, который не совпадает вообще ни с чем, а это хуже, чем совпадение с Python. Лучше замените библиотеку.
Решает ли эту проблему запуск настоящего браузера?
Да, для TLS-слоя, ведь настоящий Chrome отправляет настоящее рукопожатие Chrome. Но это обходится в сотни мегабайт на каждый воркер и заметно более медленный запрос, так что это тяжеловесное решение узкой проблемы. Используйте браузер, когда вам действительно нужно выполнение JavaScript на странице, и используйте имперсонирующий HTTP-клиент, когда нужно только тело ответа.
Как понять, что блокировка вызвана фингерпринтингом, а не ограничением частоты запросов?
Заметно снизьте скорость и попробуйте снова с того же клиента. Ограничение частоты запросов ослабевает, если подождать, обычно сообщает, сколько нужно ждать, и после этого работает без сбоев. Блокировке по TLS-фингерпринту время вообще не важно, поэтому первый запрос за день провалится точно так же, как сотый. Если холодный клиент падает уже на первом запросе, ожидание вам не поможет.
Меняет ли CapSkip мой фингерпринт JA3 или JA4?
Нет, и ничто в нём не должно этого делать. CapSkip работает как решатель капчи: вы передаёте ему задание, он возвращает токен, всё на вашем собственном оборудовании и без платы за каждое решение. Ваш фингерпринт принадлежит клиенту, который отправляет запрос, так что эта часть остаётся вашей задачей. На практике эти две части хорошо сочетаются, потому что клиенту, похожему на браузер, показывается решаемое задание, а не прямой отказ.
Самая короткая версия
Сначала прочитайте свой фингерпринт, ведь это займёт один запрос и снимет все споры. Если хеш не меняется, пока вы меняете user agent по кругу, проблема в рукопожатии, и никакой заголовок её не исправит. Переключитесь на имперсонирующий клиент, сохраните согласованность user agent, порядка заголовков и профиля HTTP/2 с тем, что вы имперсонируете, а затем посмотрите на свой IP. Появление проверки после этого говорит о прогрессе, а не о провале, и именно локальный сервис распознавания капчи превращает её в токен. Заметки о встраивании его в пул воркеров смотрите в разделе распознавание капчи для веб-скрейпинга. Когда дойдёте до самого кода, страница решателя капчи для Python содержит детали SDK, которые понадобятся вам дальше.
