Что не работает против Akamai в 2026, и что работает: разбор на businesswire.com

Дисклеймер. Материал ниже описывает исследование защиты Akamai Bot Manager на примере публичного сайта businesswire.com. Цель образовательная: показать, как устроена защита и как её слои выглядят со стороны клиента. Все источники данных, которые мы используем (MRSS feed и sitemap на S3), businesswire сам публикует в robots.txt для краулеров и поисковиков; работа идёт только с публично доступным контентом. Если вы применяете описанные техники в своих задачах, соблюдайте robots.txt, Terms of Service сайта и применимое законодательство (для США это CFAA, для ЕС — GDPR и Database Directive).

В предыдущей статье мы разбирали, что Akamai Bot Manager в 2026 проверяет на пяти параллельных слоях и какими инструментами эти слои закрываются. Сегодня — рабочий кейс под живую задачу: каждые 15 минут собирать новые пресс-релизы с businesswire.com по списку поисковых запросов и сохранять полный HTML вместе с метаданными.

businesswire.com — это площадка для распространения корпоративных пресс-релизов. Сайт защищён Akamai. На первый взгляд он выглядит подходящим для парсинга: есть поисковая модалка в шапке, есть листинги релизов, есть открытые URL. На практике ни одна из этих трёх вещей не работает так, как ожидаешь.

Готовое решение — связка Patchright и curl_cffi через резидентский US-прокси с закреплённой сессией, плюс два источника данных, которые businesswire сам отдаёт через robots.txt: MRSS feed и sitemap на S3. Один проход с пятью запросами укладывается в 47 секунд, без падений. Код целиком открыт на github.com/geekproxy/businesswire-scraper.

Дальше — путь снизу: что не работает и почему, потом что работает.

Карта эндпоинтов: что отдаётся, что блокируется

Все запросы ниже — через резидентский US-прокси с библиотекой curl_cffi и имитацией TLS-отпечатка Chrome 131. Это закрывает фингерпринты TLS и HTTP/2 (первый слой защиты Akamai). Дата-центровый IP не дошёл бы даже до TLS — его срежет четвёртый слой, IP reputation.

GET /                              → 200, 446 KB    ok
GET /newsroom                      → 200, 715 b     Page Unavailable
GET /newsroom/industry             → 200, 442 KB    ok
GET /newsroom/subject              → 200, 396 KB    ok
GET /newsroom/language             → 200, 358 KB    ok
GET /news/home/{id}/en/...         → 200, 2.4 KB    Akamai-проверка через JS
GET /sitemap.xml                   → 200, 46 KB     ok (только статические страницы)
GET /sitemap-index.xml             → 200, 1.5 KB    ok
GET /robots.txt                    → 200, 6 KB      ok

Два неинтуитивных момента сразу:

Корневая /newsroom отвечает 715 байтами «Page Unavailable» с обфусцированным JS-челленджем. При этом /newsroom/industry, /subject, /language (фактически листинги той же страницы по фильтрам) отдают полноценную одностраничную сборку на 350–450 KB. Akamai тут различает рутовый поисковый маршрут и листинги вокруг него: рут защищён жёстче.

Сами страницы релизов /news/home/{id}/... возвращают 2.4 KB. Это шелл с проверочным JS-скриптом, который ставит cookie и редиректит. Без валидного _abck (Akamai-кука, которую выдаёт сервер только после правильного POST с sensor_data из браузера) реального HTML не получить.

Итого: главная страница поиска /newsroom закрыта полностью. Доступны только подразделы листинга и, при наличии правильной куки, сами релизы.

Что не работает

Чистый requests со спуфом User-Agent

import requests
r = requests.get("https://www.businesswire.com/",
                 headers={"User-Agent": "Mozilla/5.0 ..."})
# HTTP 403

TLS-отпечаток urllib3 от CPython не имеет ничего общего с Chrome. Akamai определяет это в первом слое до того, как сервер прочитает HTTP-заголовки. Спуф User-Agent бесполезен.

Голый Playwright с headless Chromium

await page.goto("https://www.businesswire.com/")
# net::ERR_HTTP2_PROTOCOL_ERROR

Падает на TLS- и HTTP/2-рукопожатии на самой главной. Headless Chromium имеет узнаваемые отпечатки во втором слое (параметры SETTINGS-кадра, ALPN), Akamai реагирует не 403-м, а сбросом потока на уровне HTTP/2 — ERR_HTTP2_PROTOCOL_ERROR. Флаги --disable-features=Http2, --headless=new и плагин playwright-stealth не помогают: стелс-плагин патчит третий слой (свойства DOM и JS-среды), а до него мы просто не доходим.

--- default headless | args=[]
  FAIL: net::ERR_HTTP2_PROTOCOL_ERROR
--- headless=new
  FAIL: net::ERR_HTTP2_PROTOCOL_ERROR
--- disable-h2-features
  FAIL: net::ERR_HTTP2_PROTOCOL_ERROR
--- disable-blink-auto
  FAIL: net::ERR_HTTP2_PROTOCOL_ERROR
--- combo
  FAIL: net::ERR_HTTP2_PROTOCOL_ERROR

Воспроизводится скриптом probes/01_baseline_chromium.py в репозитории.

Прямой парсинг страницы поиска /newsroom

Даже после полного прогрева на главной (с движениями мышью и переходом на одну страницу релиза) /newsroom отвечает 715-байтовой заглушкой. Akamai требует на этом маршруте более строгого sensor_data, плюс при повторных попытках с того же IP прилетает 429. Рутовый поиск действительно защищён жёстче подразделов.

Поисковая модалка в шапке — это не настоящий поиск

На каждой странице businesswire в шапке есть иконка-лупа. Кликаешь, открывается модальное окно, набираешь «nasdaq» — выпадает список из пяти свежих релизов. Выглядит как нормальная функция, которую достаточно реверс-инженерить.

Открываем главную в Patchright, вешаем обработчик context.on("response", ...) на все ответы, кликаем кнопку, набираем запрос, ждём 15 секунд после Enter. Получаем 92 захваченных сетевых запроса. Из них на поиск тянут только три:

POST 200 https://1dfqiezxuz-1.algolianet.com/1/indexes/*/queries
POST 200 https://1dfqiezxuz-2.algolianet.com/1/indexes/*/queries
POST 200 https://1dfqiezxuz-3.algolianet.com/1/indexes/*/queries

Это Algolia. App ID 1DFQIEZXUZ, публичный поисковый ключ 3ef401672a314ef0434873a57f64bfd3 лежит прямо в HTML главной страницы. Тело запроса — пакетный поиск по нескольким индексам:

{"requests": [
  {"indexName": "prod_api::home.home",          "query": "nasdaq"},
  {"indexName": "prod_api::contacts.contacts",  "query": "nasdaq"},
  {"indexName": "prod_api::distribution.distribution", "query": "nasdaq"},
  {"indexName": "prod_api::help.help",          "query": "nasdaq"},
  ...
]}

В ответе по каждому индексу пустые hits: []. Algolia ничего не нашла. А в DOM модалки при этом — 45 ссылок вида /news/home/{id}/... с заголовками. Откуда они?

Это фильтр на клиенте. Next.js встроил в свойства страницы список из ~10 свежих релизов (для бокового блока «Latest News»), а модалка поиска просто прогоняет введённую подстроку через этот список. Никакого бэкенд-поиска по пресс-релизам нет — Algolia покрывает контентные страницы сайта (help, distribution, contacts), но не релизы.

Вывод: реверс-инженерить нечего. Полноценного публичного API для поиска по релизам у businesswire нет. Если задача — найти релиз по подстроке, нужно либо подключать внешний поиск (Google News с фильтром site:businesswire.com, или платный сервис обхода), либо искать альтернативный источник.

Скрипт: probes/04_reverse_search_modal.py.

Поворот через robots.txt

Стандартный приём, который применяет каждый, кто хоть раз скрейпил продовый сайт, и о котором почему-то редко пишут в гайдах по обходу: открыть /robots.txt и прочитать его глазами до того, как ломать защиту.

$ curl https://www.businesswire.com/robots.txt | grep -i sitemap

Sitemap: https://www.businesswire.com/sitemap-index.xml
Sitemap: https://bw-prod-sitemap.s3.us-east-1.amazonaws.com/webdmz1.vaprod.businesswire.com/home/smap-bw-mod.xml
Sitemap: https://bw-prod-sitemap.s3.us-east-1.amazonaws.com/webdmz1.vaprod.businesswire.com/gn-home/gn-bw-mod.xml
Sitemap: https://feed.businesswire.com/mrss/home/?rss=G1QFDERJXkJcFVJYWQ==

Тут лежит вся задача. businesswire сам опубликовал три альтернативных источника для краулеров, поисковиков и SEO-индексаторов:

  1. feed.businesswire.com/mrss/home/?rss=G1QFDERJXkJcFVJYWQ== — MRSS-фид на отдельном поддомене. Возвращает последние ~50 релизов с полным заголовком, описанием, ссылкой и датой публикации. Поддомен тоже под Akamai, но curl_cffi с имитацией Chrome 131 через резидентский прокси проходит сюда стабильнее, чем через главный домен.
  2. bw-prod-sitemap.s3.us-east-1.amazonaws.com/.../smap-bw-mod.xml — индекс сайтмапов на публичном AWS S3-бакете. На S3 Akamai не стоит вообще, любой HTTP-клиент читает без всяких танцев. Индекс ссылается на дневные сайтмапы вида 2026-05-01.xml.gz (gzip-XML), в каждом — около 450 релизов за сутки.
  3. bw-prod-sitemap.s3.us-east-1.amazonaws.com/.../gn-home/gn-bw-mod.xml — то же самое в формате Google News.

Это публичный интерфейс для краулеров. Никто его не прячет. Просто никто не смотрит в robots.txt перед тем, как рисовать диаграмму обхода TLS-отпечатка.

Финальный конвейер

   ┌──────────────────────────────────────────────────┐
   │ ШАГ 1: MRSS feed                                 │
   │ GET https://feed.businesswire.com/mrss/home/?rss=│
   │ через rotating residential US + curl_cffi        │
   │   с имитацией Chrome 131                         │
   │ → XML с ~50 свежими релизами                     │
   │   (title, description, link, pubDate)            │
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 2: фильтр по подстрокам из конфига           │
   │ если запрос (case-insensitive) есть в title или  │
   │ description — релиз попадает дальше              │
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 3: дедуп по release_id через SQLite          │
   │ оставляем только новые                           │
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 3.5: проверка живых сессий прокси            │
   │ для каждого порта из пула — короткий GET на      │
   │ главную businesswire; берём первый порт, ответ-  │
   │ ный за <8 сек с кодом 200 и >50 KB               │
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 4: прогрев Patchright на выбранном порту     │
   │ открыли главную → симуляция мыши → один релиз    │
   │ собрали Akamai-куки (_abck, ak_bmsc, bm_sv, bm_so│
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 5: curl_cffi (chrome131) с куками и тем же   │
   │ закреплённым портом. Параллельно (5 потоков)     │
   │ скачивает страницы релизов.                      │
   │ Если >50% запросов начинают тайм-аутить — повтор │
   │ шагов 3.5 и 4 с новой сессией.                   │
   └────────────┬─────────────────────────────────────┘
                ▼
   ┌──────────────────────────────────────────────────┐
   │ ШАГ 6: HTML + JSON с метаданными на диск,        │
   │ запись в SQLite                                  │
   └──────────────────────────────────────────────────┘

Отдельно, раз в сутки или после долгого простоя, можно запустить scraper.py --backfill 2026-05-15: скрипт скачает дневной сайтмап с S3 за указанную дату, отфильтрует URL по подстрокам в slug, и тем же путём дойдёт до пропущенных релизов.

Двухстадийный обход для страниц релизов

MRSS-фид под curl_cffi с имитацией Chrome 131 — это просто XML, он открывается чисто. А вот страницы релизов (/news/home/{id}/...) — уже первая категория сайтов из предыдущей статьи: нужна валидная _abck, которую сервер выдаёт только после успешной отправки sensor_data из настоящего браузера.

Решение оттуда же — двухстадийный конвейер.

Стадия 1: прогрев в Patchright. Patchright это форк Playwright с патчами Chromium на двоичном уровне, которые скрывают признаки автоматизации. Открываем главную через закреплённую US-сессию резидентского прокси, симулируем движение мыши и прокрутку (это для четвёртого слоя Akamai, поведенческого), переходим на одну страницу релиза, собираем все куки.

async with async_playwright() as p:
    browser = await p.chromium.launch(headless=True, channel="chromium",
                                       args=["--no-sandbox"])
    context = await browser.new_context(
        proxy={"server": "http://rs.geekproxy.io:10000",
               "username": "USER__cr.us", "password": "PASS"},
        user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                   "AppleWebKit/537.36 (KHTML, like Gecko) "
                   "Chrome/131.0.0.0 Safari/537.36",
        viewport={"width": 1366, "height": 900},
        locale="en-US",
        timezone_id="America/New_York",
    )
    page = await context.new_page()
    await page.goto("https://www.businesswire.com/", wait_until="domcontentloaded")
    await page.wait_for_timeout(12000)        # даём sensor отработать
    await page.mouse.move(120, 200)
    await page.mouse.move(400, 350, steps=12)
    await page.mouse.wheel(0, 600)
    await page.wait_for_timeout(1500)
    await page.goto(SAMPLE_RELEASE_URL, wait_until="domcontentloaded")
    await page.wait_for_timeout(5000)
    cookies = await context.cookies()

Из собранных кук нам нужны _abck, ak_bmsc, bm_sv, bm_so — все четыре Akamai ставит после правильного sensor_data.

Стадия 2: пакетное скачивание через curl_cffi. Создаём Session с имитацией Chrome 131, переносим в неё куки и ходим через тот же закреплённый прокси (важно: IP не должен меняться внутри одного потока действий).

from curl_cffi import requests as ccffi

proxy = "http://USER__cr.us:[email protected]:10000"
s = ccffi.Session(impersonate="chrome131", timeout=30,
                  proxies={"http": proxy, "https": proxy})
for c in cookies:
    if c["domain"].endswith("businesswire.com"):
        s.cookies.set(c["name"], c["value"], domain=c["domain"])

r = s.get(release_url, headers={
    "Accept-Language": "en-US,en;q=0.9",
    "Referer": "https://www.businesswire.com/",
    "Sec-Fetch-Site": "same-origin",
    "Sec-Fetch-Mode": "navigate",
    ...
})
# HTTP 200, ~400 KB реальной статьи

Один важный момент про соответствие браузеров между двумя стадиями. Библиотека curl_cffi умеет имитировать TLS-отпечатки разных браузеров: Chrome, Firefox, Safari, Edge — и разных их версий. Если на первой стадии прогрев делает Chromium-форк (Patchright), на второй curl_cffi должен имитировать именно Chrome — например chrome131. Если на первой стадии используется Firefox-форк Camoufox, на второй curl_cffi должен имитировать Firefox (firefox-NNN). Akamai при выдаче _abck запоминает, под каким браузером прошёл sensor_data, и на следующих запросах сравнивает заявленный браузер. Если на стадии 1 был Chrome, а на стадии 2 curl_cffi выдаёт Firefox-отпечаток, Akamai видит несоответствие и блокирует.

Пять параллельных потоков через ThreadPoolExecutor — рабочий потолок для одного закреплённого IP. Больше — и поведенческий слой Akamai начинает помечать сессию как подозрительную.

Воспроизводится скриптом probes/03_two_stage_bypass.py.

Сессия прокси — это переменная, а не константа

В теории резидентская сессия с закреплением — это «один IP на 5–120 минут». На практике у любого провайдера резидентских прокси периодически случаются такие IP:

  • холодный — домашний роутер давно не в сети, но его адрес ещё в пуле;
  • замедленный — Akamai специально снижает скорость ответа конкретному IP, который засветился ранее;
  • загруженный канал.

Если ваша сессия попала на такой IP, все запросы через неё ползут или зависают до конца сессии. Один тайм-аут — шум, цепочка — сигнал.

На Geekproxy.io есть два независимых способа держать пул закреплённых сессий.

Первый способ — порты. Каждый порт в диапазоне 10000–10010 это отдельная сессия со своим закреплённым IP. Получается 11 параллельных «слотов»: пишем в конфиге диапазон, перед прогревом скрипт пробегает по портам.

Второй способ — параметр sessid в логине. К имени пользователя добавляется суффикс ;sessid.<любая строка или число>, и сессия закрепляется на IP, который этот идентификатор сейчас представляет, в среднем на 30 минут. Меняя идентификатор, получаем произвольное количество параллельных сессий: sessid.a1, sessid.a2 и так далее. Этот способ полезен, когда нужно больше слотов, чем количество доступных портов, или когда хочется создавать «сессию под задачу» с заранее известным именем (удобно для логирования).

Для нашего скрейпера я выбрал пул портов как основной механизм: он стабильнее в логах (видно, какой именно порт обслужил какой релиз), и 11 слотов в среднем перекрывают потребности 15-минутного интервала. Параметр sessid оставлен опциональным — конфигурация в config.yaml это допускает, если порты вдруг кончатся.

Перед прогревом скрипт пробегает по пулу, на каждом порту делает короткий GET на главную businesswire и берёт первый порт, который ответил кодом 200, отдал больше 50 KB и уложился в 8 секунд.

def pick_healthy_sticky_port(cfg, log):
    for port in cfg.proxy_sticky_ports:
        proxy = f"http://...:{port}"
        t0 = time.time()
        try:
            r = ccffi.get(cfg.proxy_health_url,
                          impersonate="chrome131",
                          proxies={"http": proxy, "https": proxy},
                          timeout=cfg.proxy_health_timeout)
        except Exception:
            continue
        if r.status_code == 200 and len(r.text) > 50000:
            return port
    return None

Из реального лога одного прогона:

sticky probe port 10000: Timeout (8.0s)
sticky probe port 10001: HTTP 200 447924 bytes (2.4s) — picked
warmup: launching Patchright on sticky port 10001
warmup: 32 cookies (Akamai: ['ak_bmsc', 'bm_s', 'bm_so', 'bm_sv'])
saved 2/2 ... pass elapsed 47.4s

Порт 10000 завис на тайм-ауте, скрипт за 2.4 секунды выбрал 10001, дальше всё прошло чисто. Без пула прогон ждал бы по 30 секунд на каждом скачивании.

Если посреди прогона больше половины скачиваний начинают тайм-аутить, скрипт повторяет шаги проверки и прогрева на новой сессии, и ретраит только провалившиеся. Это аварийный обход прямо в процессе.

Цифры

Сценарий Время
Один прогон, MRSS + 5 запросов, 0–2 новых релиза 30–60 сек
Один прогон, MRSS + 20 запросов, 2–6 новых релизов 45–110 сек
Backfill за сутки, ~450 URL в сайтмапе, 10–30 матчей 5–10 мин
Сам по себе прогрев 30–35 сек
Одна страница релиза через curl_cffi на стадии 2 0.5–1.5 сек

На тестовом стенде 5 из 5 и 2 из 2 без падений. Куки от одного прогрева работают на десятках последовательных запросов в течение прогона. Akamai не помечает сессию.

Когда это сломается

Прогноз — какие компоненты «уедут» первыми и что с ними делать:

  • Patchright перестаёт получать _abck. Akamai обновил sensor или научился детектить Chromium-стелс по новому параметру. Сначала обновите patchright. Если не помогает — переходите на Camoufox (Firefox-форк со стелс-патчами) и парную имитацию Firefox в curl_cffi (firefox-NNN). Правило соответствия движков остаётся в силе.
  • MRSS-фид меняет ключ. В robots.txt всегда лежит актуальная строка Sitemap: https://feed.businesswire.com/mrss/home/?rss=.... В скрипте URL вынесен в source.mrss_url — нужно просто подправить.
  • feed.businesswire.com начинает отвечать 403 или 429. Переключите proxy.rotating_port на порт из пула закреплённых, чтобы fetch шёл через стабильный IP. Если совсем перестанет работать — поменяйте основной источник на S3-сайтмап. Заголовка и описания там нет (только URL), но slug в URL обычно содержит заголовок, и для фильтра по подстроке этого хватает.
  • Пул сессий деградирует. Расширьте sticky_port_range (попросите у провайдера больше слотов) или поменяйте страну в proxy.country.

Что универсального для других Akamai-сайтов

Метод этого поста переносится почти один в один на любой сайт под Akamai с публичным контентом:

  1. robots.txt — это первый шаг, а не последний. Очень часто там лежит ссылка на фид или сайтмап на отдельном поддомене или CDN, фактически вне основной защиты. Стоит 5 секунд, экономит часы.
  2. Карта защиты по эндпоинтам. Защита Akamai неоднородная: рутовый поисковый маршрут обычно строже, чем листинги вокруг него, а sitemap и RSS часто открыты. Проверяйте каждый эндпоинт отдельно простым curl_cffi до того, как тянуться за Patchright.
  3. Перехват сети в браузере за пять минут показывает, у вас бэкенд-поиск или клиентский фильтр. Эта проверка часто экономит недели реверс-инженеринга того, чего нет.
  4. Двухстадийный обход «прогрев в Patchright плюс curl_cffi с куками» — универсальный паттерн для всех строгих Akamai-сайтов с релиз-контентом (новости, статьи, каталоги).
  5. Сессию прокси держите как пул, а не как одну точку отказа. Независимо от провайдера, резидентские адреса периодически деградируют. План Б (другая сессия из пула) должен быть в коде с самого первого коммита.

Репозиторий

github.com/geekproxy/businesswire-scraper — полный код, конфиги, четыре probe-скрипта, которыми мы шли по этой истории. Запуск:

git clone https://github.com/geekproxy/businesswire-scraper.git
cd businesswire-scraper
python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
./venv/bin/patchright install chromium
cp config.yaml.example config.yaml
cp .env.example .env
# заполнить BW_PROXY_USERNAME, BW_PROXY_PASSWORD в .env
./venv/bin/python3 scraper.py

Резидентские прокси для этого кейса — наши, geekproxy.io. Закреплённые сессии на отдельных портах, гео-привязка US/EU/AS, IP из реальных ISP. Сразу подходят для описанного конвейера.

Резидентские прокси, которые проходят Akamai-sensor

Закреплённые сессии на отдельных портах, реальные ISP ASN, гео US/EU/AS.

Получить резидентские прокси

We use cookies to improve user experience. By clicking "Yes, I agree", you consent to this use of cookies.

Привилегия первых: В честь глобального запуска [мы даем скидку 50%+] на все датацентр тарифы. Эксклюзивное предложение для наших стартовых партнеров.