Значительная часть нашей работы — клиенты метаданных медиа, обогащение каталогов, архивирование — зависит от надёжного чтения публичных веб-страниц. Проблема редко в самих данных; проблема в том, что значительная часть инфраструктуры обнаружения ботов настроена против скриптовых атак и в итоге ошибочно классифицирует любой корректно ведущий себя не-браузерный клиент как такую атаку. Это проявляется по двум разным осям:
- «Что ты такое?» — 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.