Все статьи

· Casimiro Ferreira· 6 мин чтения

Компонуемые, готовые к работе сессии requests для устойчивого доступа к публичным данным

  • HTTP
  • Scraping
  • Cloudflare
  • Anti-Bot
  • Python
  • Open Source

Значительная часть нашей работы — клиенты метаданных медиа, обогащение каталогов, архивирование — зависит от надёжного чтения публичных веб-страниц. Проблема редко в самих данных; проблема в том, что значительная часть инфраструктуры обнаружения ботов настроена против скриптовых атак и в итоге ошибочно классифицирует любой корректно ведущий себя не-браузерный клиент как такую атаку. Это проявляется по двум разным осям:

  • «Что ты такое?» — Cloudflare и ему подобные помечают запросы не за то, что вы запрашиваете, а за то, как вы выглядите на проводе: ваше TLS-рукопожатие, ваш отпечаток JA3, способны ли вы пройти JavaScript-испытание. Обычное рукопожатие requests совершенно не похоже на браузерное, поэтому оно попадает под проверки, нацеленные на скриптовые злоупотребления, даже когда сам трафик безобиден.
  • «Кто ты такой?» — репутация IP и ограничения по частоте полностью игнорируют ваш отпечаток; они считают, сколько запросов приходит с одного адреса, что может наказать единственного корректно ведущего себя клиента так же легко, как и злоупотребляющего.

Эти две оси ортогональны, поэтому мы отвечаем на них двумя небольшими библиотеками, которые аккуратно складываются в стек: unblock_requests отвечает на вопрос что ты такое, а anon_requests отвечает на вопрос кто ты такой. Обе являются прямыми заменами сессии requests в повседневном коде. Эта статья посвящена именно этому транспортному слою — части, связанной с байтами на проводе, — а не разбору или конвейеру, который располагается поверх него.

Область применения, прямо говоря: эти транспорты предназначены только для публичных, не требующих аутентификации страниц. Они соблюдают robots.txt и любой объявленный crawl-delay — о том, как мы это проверяем перед тем, как писать скрапер, см. нашу статью про robots.txt и карты сайта — и каждый клиент, построенный на их основе, ограничен низким объёмом запросов, так что целевой источник никогда не видит от нас ощутимой нагрузки. Это не дисклеймер, добавленный задним числом; это реальное инженерное ограничение на то, как используются эти сессии, потому что устойчивый клиент, который при этом ведёт себя невежливо, сводит на нет собственное назначение.

Проектное ограничение: сохранить форму requests

Сессии unblock_requests наследуются от requests.Session и переопределяют только request() — всё остальное (.get(), .post(), cookies, заголовки, семантика контекстного менеджера) наследуется, поэтому любой код, типизированный под requests.Session, принимает их без изменений. Сессии anon_requests оборачивают, а не наследуют — они предоставляют те же методы-глаголы и тот же интерфейс контекстного менеджера, но пересоздают свою внутреннюю сессию при каждой ротации:

from unblock_requests import CloudflareSession   # alias: Session
import requests

s = CloudflareSession(flaresolverr_url="http://your-flaresolverr-host:8191")
html = s.get("https://www.progarchives.com/artist.asp?id=1").text
assert isinstance(s, requests.Session)            # True

Вот вся этическая и инженерная позиция в одной строке: мы не автоматизируем браузер-как-пользователя, мы создаём устойчивый HTTP-клиент для данных, которые уже являются публичными. Ни на чьём экране не появляется браузер с интерфейсом, и ничему на критическом пути не нужен дисплей.

Слой первый: unblock_requests и его транспорты

unblock_requests делает обычный Python-клиент совместимым с проверками обнаружения ботов, настроенными на браузеры. Вы выбираете транспорт с помощью аргумента mode= (или переменной окружения UNBLOCK_REQUESTS_TRANSPORT — явные аргументы всегда побеждают). Четыре основных:

РежимЧто он делает
curl_cffi (по умолчанию)Имитация TLS/JA3 Chrome через curl_cffi. Проходит как рукопожатие в форме браузера в большинстве сетей без дополнительной инфраструктуры.
requestsОбычный requests, без имитации.
flaresolverrПроксирует через безголовый браузер FlareSolverr, который решает JS-испытание — живые данные.
waybackЧитает последний снимок Internet Archive — устаревший, но ничего не требует.

Режим по умолчанию, curl_cffi, — это дешёвая победа. Большинство вердиктов «ты бот» сводятся к несовпадению отпечатка TLS: штатный requests (через OpenSSL) делает рукопожатие, совершенно не похожее на Chrome. curl_cffi имитирует реальную сборку Chrome (impersonate="chrome" по умолчанию), так что рукопожатие и JA3 совпадают, и проверка просто проходит. Никакого выполненного JavaScript, никакого запущенного браузера.

Когда сайт эскалирует до настоящего интерактивного JS-испытания, curl_cffi уже недостаточно — что-то должно выполнить испытание. Здесь вступает режим flaresolverr: экземпляр FlareSolverr, который вы размещаете сами, выполняет решение в безголовом браузере вне вашего процесса, а unblock_requests просто отправляет туда POST и извлекает решённый HTML из ответа. Установка flaresolverr_url выбирает этот режим автоматически:

CloudflareSession(flaresolverr_url="http://host:8191")          # solve live
CloudflareSession(mode="wayback")                              # force archive
CloudflareSession(flaresolverr_url="http://host:8191",
                  wayback_fallback=True)                       # live, archive on failure

Плавная деградация к архиву

У инфраструктуры бывают плохие дни — FlareSolverr не работает, сайт недоступен, испытание сейчас нерешаемо. Вместо того чтобы провалить всю задачу, сессия может откатиться к Wayback Machine. Обнаружение испытаний эвристическое: небольшой помощник is_challenge() обнюхивает первую часть тела в поисках характерных маркеров промежуточной страницы Cloudflare («just a moment», challenge-platform, cf_chl_opt, cf-mitigated). При заблокированном GET, если wayback_fallback включён, сессия разрешает последний снимок через API доступности archive.org и возвращает его сырые байты (сырая форма …id_/, без панели инструментов и переписывания ссылок). archive.org не защищён Cloudflare, поэтому обычный requests до него добирается.

Две заметки по реализации, которые стоит знать: в режимах wayback и flaresolverr результатом является синтезированный, но подлинный requests.Response, построенный из полученного HTML — поэтому stream=, пользовательские адаптеры и пул соединений там не применяются, тогда как режимы requests/curl_cffi полностью нативны. И откат срабатывает только для GET-ов; мы никогда не воспроизводим молча изменяющий запрос из архива.

Слой второй: anon_requests и ротация IP

Ортогональная проблема — это репутация IP. Даже идеально похожее на браузер рукопожатие может попасть под ограничение частоты, если все запросы приходят с одного адреса — эвристики на основе объёма смотрят на адрес, а не на отпечаток. anon_requests распределяет нагрузку по адресам с помощью RotatingProxySession (собранные публичные прокси, опциональная валидация, SOCKS5/HTTP) и RotatingTorSession (ротация Tor-цепочек), так что низкообъёмный клиент никогда не будет принят за того, кто долбит сайт с одного IP. Каждый запрос выходит через новый выход, а мёртвые прокси выводятся из ротации при сбое соединения.

from anon_requests import RotatingProxySession, ProxyType

with RotatingProxySession(proxy_type=ProxyType.SOCKS5, validate=True) as s:
    print(s.get("https://ipecho.net/plain", timeout=5).text)  # a new IP each time

Композиция: распределённая нагрузка и совместимое рукопожатие одновременно

Эти две библиотеки спроектированы так, чтобы складываться в стек, а не пересекаться. Сессии anon_requests принимают session_factory — любой вызываемый объект, возвращающий requests.Session, по умолчанию requests.Session. Настройки ротации и прокси применяются к тому, что эта фабрика возвращает. Таким образом, вы внедряете CloudflareSession в качестве фабрики и получаете оба поведения в одном объекте:

from anon_requests import RotatingProxySession
from unblock_requests import CloudflareSession

session = RotatingProxySession(
    session_factory=lambda: CloudflareSession(flaresolverr_url="http://host:8191"),
)
session.get(url)   # spreads load across IPs *and* uses a browser-compatible handshake

Ротируемый прокси проходит через каждый транспорт — включая внутрь FlareSolverr, который управляет своим безголовым браузером через поле proxy запроса на решение. Так что весь запрос — рукопожатие, решение испытания и выходной IP — остаётся согласованным от начала до конца, что попросту является корректным поведением для клиента, который не пытается выглядеть более чем одним посетителем.

Почему именно такая форма

Хранение каждой задачи в её собственном тонком подклассе requests.Session означает, что вызывающая сторона выбирает только то, что ей нужно — лишь имитацию TLS, полный стек ротации-плюс-решения или что-то посередине — заменяя конструктор, а не переписывая свой HTTP-код. Дорогой, тяжеловесный инструмент (настоящий браузер) остаётся вне процесса во FlareSolverr и вызывается только тогда, когда JS-испытание действительно этого требует; типичный случай — дешёвое имитированное рукопожатие. А когда живая сеть отказывает, отвечает архив.

Шов session_factory — это то место, где происходит композиция, и именно это сохраняет библиотеки независимо расширяемыми: добавьте режим транспорта в unblock_requests, и anon_requests скомпонует его бесплатно. Устойчивый доступ к публичным данным, сделанный чисто.

Обе являются FOSS и допускают самостоятельное размещение: unblock_requests и anon_requests.

Эти транспорты обеспечивают работу всех наших скраперов музыкальных баз данных. Для разведки сайта перед тем, как строить любой скрапер, см. sitemapper и статью про robots.txt и sitemaps.