# Как собирать цены на отели без браузера: реверс-инжиниринг цепочки запросов Radisson

Браузер — самый простой способ собирать данные с сайта, защищённого от ботов. Он же самый медленный и самый дорогой. В этой статье мы строим скрапер другого типа для [Radisson Hotels](https://www.radissonhotels.com): он отправляет только те HTTP-запросы, которые возвращают данные, и проходит Akamai Bot Manager, ни разу не запуская браузер.

Radisson — хороший полигон для сбора цен на отели. Цен нет в HTML: они приходят из JSON API, к которому страница обращается после загрузки, а прежде чем пропустить эти запросы, сайт прогоняет три проверки Akamai.

Покажем, как мы к этому пришли. Мы записали одну настоящую сессию браузера, нашли среди 148 запросов к Radisson четыре нужных вызова API, выяснили, какие проверки Akamai запускает сайт и где, и воспроизвели всю цепочку на Go. Затем мы прогнали её через резидентные прокси [Massive](https://joinmassive.com) в Великобритании.

> **Основные выводы**
>
> - Результаты поиска Radisson приходят из четырёх JSON-эндпоинтов. Именно на двух из них, определении города и запросе цен, Akamai блокирует подозрительную сессию.
> - При загрузке страницы сайт запускает три проверки Akamai: SBSD, сенсор `_abck` и пиксельный челлендж. Скрапер на запросах должен ответить на все три в том же порядке, что и страница.
> - Из 100 сессий через британские резидентные IP Massive все до одной прошли Akamai, а 490 из 500 запланированных поисков вернули цены (98 %).
> - Прохождение Akamai обошлось в 520 КБ прокси-трафика на сессию, и три четверти из них пришлись на два JavaScript-файла. Каждый поиск после этого стоил 12,5 КБ.
> - Сессия, прошедшая Akamai, продолжает работать. Пять сессий выполнили по 100 поисков каждая, и ни одну ни разу не остановили, а трафик составил 18 КБ на поиск с учётом всего.

## Почему на запросах

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

Подвох в том, что нужно точно знать, какие запросы отправлять. Защита от ботов смотрит не только на то, что вы запрашиваете. Она смотрит и на то, как: на TLS-рукопожатие, порядок заголовков HTTP/2, cookies и JavaScript-челленджи, на которые настоящий браузер отвечает в фоне. Поэтому работа начинается с реверс-инжиниринга цепочки запросов.

## Шаг 1: записываем настоящую сессию

Для начала мы выполнили поиск на Radisson в Chrome, пустив трафик через [powhttp](https://hypersolutions.co/blog/how-to-use-powhttp-web-scraping), отладочный HTTP-прокси. Мы открыли главную страницу, ввели «Amsterdam», выбрали даты и запустили поиск.

Мы используем powhttp, а не DevTools браузера, по одной причине. DevTools показывает подчищенную версию каждого запроса. powhttp показывает то, что на самом деле ушло в сеть: реальный порядок заголовков, псевдозаголовки HTTP/2, TLS-отпечаток и каждый заголовок cookie, который отправил браузер. Именно эти детали проверяет защита от ботов, и именно их скрапер должен скопировать.

![Список запросов в powhttp для записанного поиска на Radisson, отфильтрованный по хостам, оканчивающимся на radissonhotels.com](https://cdn.sanity.io/images/6hf2slow/production/bdd569f92c8c3614f766f6f5d35e8a570f104f74-2316x1224.png "Записанный поиск, отфильтрованный по собственным доменам Radisson: страница, её скрипты и изображения, эндпоинты Akamai и вызовы API.")

Один этот поиск дал 568 запросов к 46 хостам. Большая часть — шум: Google Maps, аналитика, баннер согласия на cookies, изображения. После фильтра по собственным доменам Radisson осталось 148 запросов: JavaScript-бандлы, изображения, скрипты Akamai и 38 вызовов JSON API.

## Шаг 2: находим запросы, которые возвращают цены на отели

Цен на отели в HTML не было. Страница результатов — это JavaScript-приложение, которое подгружает их после рендеринга, поэтому мы прошлись по 38 вызовам API и посмотрели, что возвращает каждый. Четыре из них содержат всё, что нужно для поиска:

- `GET /zimba-api/autocomplete/amsterdam` возвращает введённое место в виде Google place ID.
- `GET /zimba-api/search-result/hotels/search?placeId=…` возвращает коды отелей Radisson для этого места вместе с расстояниями.
- `POST /availability-api/multiple-hotels-rates` возвращает самую низкую цену каждого отеля на заданные даты и для заданного числа гостей.
- `GET /zimba-api/search-result/hotels?code=…` возвращает название отеля, адрес, звёздность и рейтинг по отзывам.

Главный из них — запрос цен. Он принимает даты, гостей и список кодов отелей в JSON-теле и возвращает цену по каждому отелю, итог с налогами и число оставшихся номеров.

![powhttp с телом запроса multiple-hotels-rates и его JSON-ответом](https://cdn.sanity.io/images/6hf2slow/production/44408488a04a170bf57d722821d932ab49236319-1200x1555.png "Запрос цен: на входе даты, гости и коды отелей, на выходе цена по каждому отелю.")

Остальные 34 вызова загружают для страницы подписи, валюты, курсы обмена, номера телефонов и контент программы лояльности. Скраперу ни один из них не нужен.

## Шаг 3: копируем запросы в точности

Дальше мы посмотрели на заголовки этих четырёх вызовов. Большинство из них отправляет любой fetch в Chrome. Несколько заголовков специфичны для Radisson:

- `x-source-of-business: desktop` в каждом вызове, а также `x-version: V2` и `x-currency` в запросе цен.
- `transactionid` — 40-символьное значение, одинаковое во всех вызовах в рамках одного визита.

`transactionid` выглядел как токен, выданный сервером. Это не так. Поиск по JavaScript сайта в powhttp по строке `transactionId` вывел на функцию, которая его создаёт: 40 случайных цифр в системе счисления с основанием 15, которые генерируются при первой загрузке страницы и хранятся в session storage браузера. Поэтому наш скрапер тем же способом генерирует такое значение для каждой сессии.

![Результаты поиска StoreTransactionId в powhttp, открытые в home.js на функции, которая генерирует transactionid](https://cdn.sanity.io/images/6hf2slow/production/573e831ceba36a2415fef8927409e3124942d812-2881x1550.png "transactionid — это 40 случайных цифр в системе с основанием 15, которые создаются в браузере и хранятся в session storage.")

Затем мы скопировали порядок заголовков. Chrome отправляет заголовки в фиксированном порядке, который зависит от типа запроса: у загрузки страницы, скрипта, `fetch()` и отправки формы он свой. Мы скопировали каждый порядок из записи, включая место заголовка cookie.

Для самого соединения мы взяли [tls-client](https://github.com/bogdanfinn/tls-client) для Go: он открывает соединения с TLS- и HTTP/2-отпечатком Chrome. У стандартного HTTP-клиента Go TLS-отпечаток, которого нет ни у одного браузера, а именно на TLS-отпечаток защита от ботов смотрит одним из первых.

## Шаг 4: выясняем, где стоит Akamai

Когда запросы за данными стали известны, оставалось понять, что нужно Akamai, чтобы их пропустить. Ответ — в HTML главной страницы и в запросах, которые страница делает сразу после загрузки.

Страница подключает три скрипта Akamai:

- **Скрипт сенсора** — по длинному, на вид случайному пути вроде `/hLJOkaPJ/…/XMmkuCU0`. Он собирает данные о браузере и трижды отправляет их обратно на тот же путь. Каждый ответ обновляет cookie `_abck`, и как только в нём появляется `~0~`, Akamai считает сессию валидной.
- **Скрипт SBSD** — по похожему пути с параметром `?v=`. SBSD (State Based Scraping Detection) — вторая проверка Akamai. Во время работы страница отправляет пейлоады на тот же путь, в нашей записи — трижды, и каждый ответ обновляет cookie `bm_s`. Наш скрапер отправляет два, и этого хватило.
- **Скрипт пикселя** — по пути `/akam/13/…`, а в саму страницу прописано число. Страница отвечает на него один раз, отправляя форму на тот же путь с префиксом `pixel_`.

![HTML главной страницы Radisson: число пикселя Akamai в head и теги скриптов сенсора и SBSD в конце body](https://cdn.sanity.io/images/6hf2slow/production/7b219dad0603443a4d9d6a656dd55942b76e1a2d-2015x536.png "Главная страница загружает скрипт сенсора и скрипт SBSD от Akamai по случайным на вид путям и прописывает число пикселя в head.")

![powhttp: POST-запрос сенсора со статусом 201, успешный ответ и устанавливаемый им cookie _abck](https://cdn.sanity.io/images/6hf2slow/production/bc4d033eff1c203a0e75bef2d5808e1b304abcfd-2015x783.png "Отправка данных сенсора: Akamai отвечает 201 и устанавливает cookie _abck, содержащий ~0~.")

Здесь важны две детали. Пути к скриптам меняются, поэтому скрапер должен считывать их с каждой загруженной главной страницы, а не хардкодить. За время наших тестов путь SBSD менялся несколько раз. А на некоторых IP главная страница — вовсе не главная страница. Это короткая страница челленджа SBSD с параметром `t=` у скрипта. Скрапер должен ответить на этот челлендж и загрузить страницу заново, прежде чем делать что-либо ещё.

Ни одну из этих проверок не пройти, скопировав записанный пейлоад. Каждый пейлоад генерируется из скрипта, cookies, user agent и IP текущей сессии. Мы генерируем их через [Hyper Solutions API](https://hypersolutions.co/products/akamai): скрапер скачивает скрипт через свой прокси, передаёт его в API вместе с cookies и IP сессии и отправляет на сайт пейлоад, который вернул API. Для этого мы использовали наш [Go SDK](https://github.com/Hyper-Solutions/hyper-sdk-go).

Как работает каждая из этих проверок по отдельности, с кодом SDK на Go, Python и JavaScript, мы разбираем в нашем [руководстве по Akamai без headless-браузера](https://hypersolutions.co/blog/bypass-akamai-without-headless-browser).

## Шаг 5: сверяем скрапер с браузером

Перед масштабированием мы пустили через powhttp и сам скрапер, чтобы его запросы оказались в том же инструменте рядом с браузерными. Так их можно сравнить напрямую:

- TLS-отпечаток совпал с отпечатком Chrome. Хеши JA4 браузера и скрапера отличались только случайным значением GREASE, которое Chrome вставляет намеренно.
- Порядок заголовков совпал для всех типов запросов.
- Заголовок cookie был разбит на отдельные заголовки, по одному на каждый cookie, как это делает Chrome по HTTP/2.

![Запросы цен от браузера и от скрапера рядом в powhttp, заголовки в одинаковом порядке](https://cdn.sanity.io/images/6hf2slow/production/25f8a22d0a860b08e005651a8ad47dacc2b1aec6-2881x1240.png "Запрос браузера (слева) и запрос скрапера (справа): те же заголовки в том же порядке, различаются только значения вроде дат и валюты.")

Если что-то не так, вы найдёте это именно здесь, и найти это здесь гораздо дешевле, чем потом по росту доли блокировок.

## Готовая цепочка

В сборе одна сессия скрапинга делает следующее:

1. Определяет свой выходной IP, потому что пейлоады Akamai генерируются под него.
2. Загружает главную страницу. Если это страница челленджа SBSD, отвечает на челлендж и загружает её снова.
3. Скачивает скрипт SBSD и отправляет два пейлоада SBSD.
4. Скачивает скрипт сенсора и отправляет данные сенсора, пока в `_abck` не появится `~0~`, а после первой отправки отвечает на пиксельный челлендж.
5. Ищет: один раз определяет город, затем для каждого поиска получает список отелей и отправляет запрос цен.

После этого сессия может продолжать поиски. Если Akamai отвечает на защищённый вызов челленджем, сессия отвечает на него и повторяет вызов один раз.

Код открыт: [github.com/Hyper-Solutions/radisson-scraper](https://github.com/Hyper-Solutions/radisson-scraper). Это пакет Go и небольшая утилита командной строки, которая выводит отели и цены для города в формате JSON:

<div data-block="html"><div style="background:#011627;border:1px solid rgba(250,244,236,0.08);border-radius:10px;margin:1.5rem 0;"><div style="padding:10px 18px 0;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.08em;color:rgba(214,222,235,0.6);">BASH</div><pre style="margin:0;padding:10px 18px 16px;background:transparent;color:#d6deeb;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.65;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:visible;"><code>git clone https://github.com/Hyper-Solutions/radisson-scraper.git &amp;&amp; cd radisson-scraper
export HYPER_API_KEY=your-key
export PROXY_URL='http://your-username-country-GB-session-{session}-sessionttl-30:your-password@network.joinmassive.com:65534'
go run ./cmd/radisson -city london -checkin 2026-11-20 -checkout 2026-11-22 -currency GBP</code></pre></div></div>

## Настройка прокси Massive

Каждой сессии нужен свой резидентный IP, и она должна сохранять его до конца работы. Пейлоады Akamai генерируются под один IP, поэтому если IP меняется посреди сессии, сессия перестаёт быть валидной.

В панели управления Massive есть Proxy Generator (генератор прокси), который сам собирает строку подключения. Мы выбрали резидентные прокси по HTTP и тип ротации Sticky (один IP на сессию) с Session TTL (время жизни сессии) 30 минут.

![Proxy Generator в Massive: резидентные прокси по HTTP, ротация Sticky и Session TTL 30 минут](https://cdn.sanity.io/images/6hf2slow/production/50c42e181ca54839cf29788fc38d7390fecdb58d-1403x1167.png "Ротация Sticky закрепляет один IP за каждым ID сессии, здесь на 30 минут.")

Затем в разделе Geotargeting (геотаргетинг) мы выбрали United Kingdom. Генератор упаковывает всё это в имя пользователя прокси:

![Proxy Generator в Massive с выбранной страной United Kingdom и сгенерированной командой cURL, в которой видно sticky-имя пользователя для GB](https://cdn.sanity.io/images/6hf2slow/production/b762ea2745768cb53b30ef0c29359fe77b35f9b8-921x1200.png "Если выбрать United Kingdom, имя пользователя принимает вид your-username-country-GB-session-ID-sessionttl-30.")

- `country-GB` выбирает британские резидентные IP.
- `session-<id>` закрепляет за сессией один IP. Все запросы с одним и тем же ID уходят с одного IP, а новый ID получает новый IP. Генератор подставляет случайный ID; наш скрапер ставит новый для каждой сессии.
- `sessionttl-30` сохраняет этот IP за сессией на 30 минут — дольше, чем работала любая из наших сессий.

## Результаты

Мы прогнали 100 сессий через британские резидентные IP Massive, по 10 одновременно. Каждая сессия проходила Akamai, а затем выполняла пять поисков, каждый по случайному городу из списка в 16 городов и на случайные даты на срок от двух до 12 недель вперёд. Считающий прокси на нашей машине измерял каждый байт между скрапером и Massive. Тест мы провели 22 октября 2026 г.

- **Сессий, прошедших Akamai:** 100 из 100.
- **Сессий, выполнивших все пять поисков:** 98 из 100.
- **Поисков, вернувших цены:** 490 из 500 запланированных (98 %).
- **Медианное время прохождения Akamai:** 2,6 секунды.
- **Медианное время одного поиска:** 1,5 секунды.

Обе неудачные сессии прошли Akamai, а затем получили отказ на первом же запросе цен. Обе были заблокированы окончательно: повторное прохождение Akamai на том же IP не помогло. Это IP, которому Akamai не доверяет, и решение здесь — новая сессия на новом IP.

### Сколько это стоит в прокси-трафике

![Столбчатая диаграмма прокси-трафика скрапера на запросах по этапам: бутстрап Akamai, определение города, один поиск и определение IP](https://cdn.sanity.io/images/6hf2slow/production/ab3e852b1577a97a6812ec9bd3221de0224e8c21-2400x1164.png "Средний прокси-трафик на этап через британские резидентные IP Massive.")

Прохождение Akamai, которое мы называем бутстрапом, — безусловно, самая дорогая часть сессии: 520 КБ. Поиск стоит 12,5 КБ: 5,2 КБ на список отелей и 7,2 КБ на цены. Определение города стоит 6,4 КБ, один раз на город.

![Столбчатая диаграмма прокси-трафика бутстрапа по этапам; больше всего приходится на скрипты SBSD и сенсора](https://cdn.sanity.io/images/6hf2slow/production/2e57c4b745fb13ef563d19d5771a422d4fe39187-2400x1764.png "Средний прокси-трафик на этап бутстрапа.")

Почти весь бутстрап — это две загрузки: скрипт SBSD (189 КБ) и скрипт сенсора (195 КБ). Вместе они составляют 74 % от него. Главная страница весит 64 КБ, а все POST-запросы вместе — меньше 60 КБ.

При пяти поисках на сессию это 122 КБ прокси-трафика на поиск с учётом всего. Чем дольше живёт сессия, тем меньше весит бутстрап. Во втором тесте пять сессий выполнили по 100 поисков каждая на одном бутстрапе. Все 500 поисков вернули цены, Akamai ни разу не остановил ни один из них, а трафик составил 18 КБ на поиск с учётом всего.

## Практические советы

- **Записывайте инструментом, который показывает, что уходит в сеть.** Порядок заголовков, псевдозаголовки HTTP/2 и TLS-отпечаток в DevTools не видны. А именно их проверяет защита от ботов.
- **Ищите JSON, а не HTML.** Если страница — JavaScript-приложение, её данные приходят из API. Обращаться к этому API напрямую дешевле и надёжнее, чем парсить страницу.
- **Читайте JavaScript сайта, прежде чем гадать.** Значения вроде `transactionid` выглядят как серверные токены, а оказываются случайными. Поиск по бандлу отвечает на этот вопрос за минуты.
- **Каждый раз парсите пути к скриптам Akamai.** Они меняются.
- **Проверяйте скрапер в том же инструменте, что и браузер.** Сравнение бок о бок находит неверный порядок заголовков или отпечаток раньше, чем это проявится в доле блокировок.
- **Держите один IP на сессию.** Используйте sticky-сессии, проверяйте выходной IP и, когда сессию блокируют, начинайте новую на новом IP.
- **Переиспользуйте сессии.** Бутстрап — самая дорогая часть, поэтому выполняйте на одной сессии столько поисков, сколько позволяет сайт.

## Часто задаваемые вопросы

### Можно ли собирать цены на отели без headless-браузера?

На Radisson — да. Цены приходят из JSON API тарифов. В нашем тесте скрапер, который отправляет только запросы страницы, Akamai и API с TLS-отпечатком и порядком заголовков Chrome, вернул цены для 490 из 500 запланированных поисков.

### Зачем для сбора цен на отели нужны резидентные прокси со sticky-сессиями?

Пейлоады Akamai генерируются под один IP. Если IP меняется посреди сессии, сессия перестаёт быть валидной, поэтому каждая сессия должна сохранять один резидентный IP до конца работы.

### Сколько прокси-трафика расходует скрапер отелей на запросах?

В нашем тесте прохождение Akamai стоило 520 КБ на сессию, а каждый поиск — 12,5 КБ. При пяти поисках на сессию это 122 КБ на поиск с учётом всего; при 100 поисках на одной сессии — уже 18 КБ на поиск.

## Примечания о методологии

- **Даты и масштаб.** Мы провели тесты 22 октября 2026 г. через британские резидентные IP Massive: 100 сессий, по 10 одновременно, по пять поисков в каждой. В длинном тесте работали ещё пять сессий, все пять одновременно, по 100 поисков в каждой.
- **Поиски.** Поиск — это список отелей для места плюс один запрос цен для всех его отелей, на двух взрослых и на срок от одной до трёх ночей. Данные об отелях (названия и адреса) меняются редко, поэтому мы не включали их в стоимость поиска.
- **Что учитывается.** Трафик — это всё, что наш считающий прокси видел между скрапером и Massive, включая установку соединений, TLS-рукопожатия и неудачные попытки. Вызовы Hyper Solutions API шли напрямую с нашей машины, а не через прокси, поэтому они не учтены.
- **Идентичность браузера.** Скрапер представляется как Chrome 155 на Windows, с заголовками и порядком заголовков из записанной сессии.

## Запустите сами

- **Код:** в [репозитории radisson-scraper](https://github.com/Hyper-Solutions/radisson-scraper) лежит вся цепочка на Go.
- **Прокси:** настройте sticky-сессию на резидентных прокси в Proxy Generator от [Massive](https://joinmassive.com), как показано выше.
- **Пейлоады Akamai:** [Hyper Solutions Akamai API](https://hypersolutions.co/products/akamai) генерирует пейлоады для сенсора, SBSD и пикселя; начните с [руководства по началу работы с Akamai](https://docs.hypersolutions.co/akamai-web/getting-started). Каждый новый аккаунт Hyper Solutions начинается с бесплатного пробного периода на неделю.
