Вы когда-нибудь радовались идеальному прототипу парсера, который у вас летал на демо-странице, а в проде внезапно начал ловить 403, 429, пустые HTML и «куда-то делись карточки»? Контент отрисовывается на JS, сервер требует токен, после смены IP, старая сессия перестаёт работать.

В этой статье я подробно разберу, как собирать данные устойчиво и предсказуемо, без излишней магии и с упором на реальную эксплуатацию.

Вся статья и примеры на Python.

Что разберём в парсинге сайтов

  • когда хватает requests, а когда нужен headless-браузер;

  • как вести себя «как человек»: headers, user-agent, cookies, локаль и таймзона;

  • сессии и токены — почему «новый IP = новая сессия» и что с этим делать;

  • скорость, rate-limit и бэкоффы* без бана подсети;

  • «бесконечные списки»: скролл или перехват XHR/GraphQL;

  • отдельный практический блок про ротацию IP.

*бэкофф — стратегия повторных попыток, при которой между неудавшимися попытками делается пауза, где каждая последующая пауза длиннее.

Немного о сервисе Amvera или где деплоить парсер, чтобы не ловить 429

Amvera — облачный сервис, созданный в первую очередь для простого деплоя IT-приложений различного рода. Сервис поддерживает множество окружений, таких как Python, Node.JS, JVM, Go, C#, Docker и прочие.

Важным бонусом для нас будет являться:

  1. Ротация исходящих IP из пула. Это поможет избежать большинство блокировок по IP и 429.

  2. 111 рублей на баланс сразу после регистрации даст пространство для тестов сразу на сервере.

  3. Помимо вышесказанного, сервис предоствляет бесплатное проксирование до OpenAI, GrokAI, Gemini и множества сервисов с региональными ограничениями.

Итак, вернемся к теме.

1. Базовый случай, когда requests реально достаточно

Если страница статична или есть открытый JSON-эндпоинт — берите requests плюс HTML-парсер (lxml, BeautifulSoup, parsel). Это быстро, дёшево и легко масштабируется.

Ключевые правила парсинга в таком случае:

  • отправляйте реалистичные заголовки (headers с UA);

  • фиксируйте Accept-Language и Referer, если сайт регионозависимый;

  • на ошибках 5xx и 429 используйте бэкофф с Retry-After.

Минимальный пример:

# pip install bs4 requests lxml

import requests
from bs4 import BeautifulSoup

headers = { "User-Agent": "Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36", "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.7", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}

r = requests.get("https://какой-то.адрес", headers=headers, timeout=30)
r.raise_for_status()

soup = BeautifulSoup(r.text, "lxml")
titles = [t.get_text(strip=True) for t in soup.select(".title")]

Когда этого мало:

  • контент рисуется на клиенте после загрузки;

  • нужна интеракция пользователя;

  • сервер проверяет браузерные сигналы и сессии.

То есть этого вполне достаточно для простых и демо-сайтов.

2. Когда без headless-браузера никуда

Признаки:

  • пустой HTML, а в DevTools браузера видно богатый DOM;

  • множество XHR/Fetch/GraphQL после загрузки;

  • чувствительность к sec-ch-ua, WebGL, плагинам, локалям, таймзоне и прочему.

Рекомендация — Playwright с Chromium. Он даёт реалистичное окружение, нормальные sec-ch-ua.

Короткий пример загрузки страницы:

# pip install playwright  
# playwright install

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    ctx = browser.new_context(
        user_agent="Mozilla/5.0 ... Chrome/124.0.0.0 Safari/537.36",
        locale="ru-RU",
        timezone_id="Europe/Moscow",
        viewport={"width": 1280, "height": 800},
    )
    page = ctx.new_page()
    page.goto("https://example.com/catalog", wait_until="networkidle", timeout=45000)
    html = page.content()
    print(html)
    browser.close()
    ctx.close()

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

  • не урезайте браузер излишне. Чем «натуральнее» он выглядит, тем меньше триггеров на стороне защиты;

  • фиксируйте локаль и таймзону, это влияет на цены, формат дат и сами селекторы;

  • если видите XHR с JSON - парсите JSON, а не HTML.

Заголовки и user-agent

Поддерживать актуальные User-Agent самому неудобно. Используйте библиотеки, которые держат множество UA и умеют выбирать реальные:

  • fake-useragent

  • random_user_agent

Использование:

# pip install fake-useragent

from fake_useragent import UserAgent
ua = UserAgent()

headers = { "User-Agent": ua.random, "Accept-Language": "ru-RU,ru;q=0.9,en;q=0.7", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}

Важно:

  • старые UA чаще попадают под подозрение;

  • не добавляйте экзотических заголовков, которых реальный браузер не шлёт.

4. Куки, токены и «новый IP = новая сессия»

Многие сайты жестко привязывают сессию к IP-адресу.
Если вы получили сессионную куку на IP A, а потом внезапно пошли с ней через прокси B, сервер это почти наверняка воспримет как подозрительное поведение. В лучшем случае он просто обнулит сессию, в худшем — сразу вернёт 403.

Выводы:

  • куки нужно хранить в связке ip - cookies;

  • при смене прокси создавайте новую сессию, не reuse (используя повторно) старую;

  • не делитесь одними и теми же куками между разными IP;

  • CSRF-токены и прочие «одноразовые» маркеры. Это тоже часть конкретной сессии, их нужно обновлять.

Удобный шаблон на Python

  1. Сначала «прогреваем» сессию в headless-браузере — принимаем cookies, логинимся (если нужно).

  2. Сохраняем куки в storage_state.

  3. Используем их уже в сессии requests.Session, но строго на том же IP.

Простейшая схема:

def bootstrap_session_from_browser(proxy):
    # возвращаем готовую сессию

pool = {
    proxy: bootstrap_session_from_browser(proxy)
    for proxy in proxies
}

def fetch(url):
    proxy, sess = choose_proxy(pool)
    r = sess.get(url, timeout=30)

    # если сервер выкинул сессию - пересоздаём
    if r.status_code in (401, 403):
        pool[proxy] = bootstrap_session_from_browser(proxy)
        r = pool[proxy].get(url, timeout=30)

    r.raise_for_status()
    return r.text

5. Скорость, rate-limit и бэкофф

Даже идеальные заголовки не спасут, если вы забьёте сайт десятками запросами в секунду. Правила простые:

  • ограничивайте частоту запросов на домен.

  • применяйте бэкофф.

  • придерживайтесь Retry-After.

  • планируйте очереди с запасом по времени.

Минимальная схема бэкоффа:

import time, random, requests def  get_with_backoff(sess, url, retries=5, base=0.5, factor=2.0): for attempt in  range(retries):
        r = sess.get(url, timeout=30) if r.status_code == 429:
            ra = r.headers.get("Retry-After")
            wait = float(ra) if ra else base * (factor ** attempt)
            time.sleep(wait + random.uniform(0, 0.5)) continue r.raise_for_status() return r.text raise RuntimeError("слишком много попыток")

6. Бесконечная прокрутка: два способа

Наверняка вы встречали сайты, где товары или посты подгружаются не сразу, а появляются по мере прокрутки вниз. Это и есть бесконечная прокрутка. Она удобна пользователям, но усложняет жизнь парсеру: страница сама по себе неполная и её нужно докручивать.

Есть два подхода, как это обойти:

Подход 1 - скроллим страницу как человек

Самый прямой и простой способ — имитировать прокрутку в headless-браузере.
Код буквально "крутит колёсико мышки", ждёт подгрузку и снова скроллит. Так можно дотянуться до самого низа страницы.

Минус: это медленно, потому что вы действительно ждёте загрузку и отрисовку элементов.

page.mouse.wheel(0, 2400)
page.wait_for_timeout(800) # подождать, пока успеет подгрузиться

Подход 2 — ищем API

Более «хакерский» и быстрый вариант: открыть DevTools и посмотреть, откуда страница тянет данные. Обычно карточки товаров или постов прилетают отдельным XHR/GraphQL запросом - например, на /api/catalog. Если найти этот запрос и повторить его напрямую в коде, то вы сразу получите весь JSON со списком карточек, минуя сам скролл.

Сценарий будет выглядеть примерно так:

  • открыть страницу через Playwright.

  • подписаться на события page.on("response", ...).

  • отфильтровать ответы по адресу (например, /api/catalog).

  • читать JSON и вытаскивать нужные данные.

Итог: если нужно быстро и надёжно — ищите API и «бейте» в него.
Если API спрятано или слишком заморочено — придётся скроллить страницу.

7. Важный бонус: пул IP в Amvera для обхода 429

Если вы деплоите парсер в Amvera, то получите важный бонус: исходящий трафик ходит через пул IP. На практике это помогает «рассредоточить» нагрузку и снизить вероятность 429 на адрес, потому что ваши запросы выходят не всегда с одного и того же IP. Чем это удобно:

  • отдельные платные прокси не требуются.

  • частота с одного конкретного IP для внешнего сайта ниже.

  • как побочный эффект — ниже шанс моментального rate-limit по адресу.

И, что важно, при регистрации в Amvera вам сразу будет доступен бонус с виде 111 рублей на баланс для тестов. Этого будет более чем достаточно, чтобы успеть настроить парсер и даже некоторое время покрутить своё приложение совершенно бесплатно.

Но есть особенности архитектуры, которые обязательно учтите:

  • нет явной кнопки или API, чтобы самостоятельно и мгновенно переключить исходящий IP. Ротация происходит без чёткого правила, рандомно под капотом инфраструктуры.

  • нельзя гарантировать, что запрос уйдет именно с нового IP.

  • если цель привязывает сессию к IP, то рандомная смена источника может порвать сессию.

  • можно настроить уведомления в бота Amvera об ошибках в логе, к примеру, о 429. Так, вы сразу узнаете, если с парсингом что-то не так.

Даже с IP-пулом не отказывайтесь от бэкоффов и разумного лимитирования. Это по-прежнему лучшая защита от 429. Ещё один практический совет: делайте задания идемпотентными, чтобы неожиданный разрыв сессии на середине не порождал дублей и мусора в хранилище.

8. Частые поломки парсинга и короткие рецепты

  • 403 сразу - проверьте свежесть UA, Accept-Language, Referer, попробуйте Playwright и реалистичный контекст

  • 429 периодически — снизьте RPS, попробуйте деплоить на Amvera.

  • Пустой HTML — страница на JS, перехватывайте XHR/GraphQL или используйте headless.

  • разный язык/валюта — фиксируйте локаль и таймзону в контексте и заголовках.

Итог

  • Начинаем с requests — дёшево и быстро.

  • Если страница живёт на JS — берём Playwright.

  • Сессии и токены часто привязаны к IP — не переносите куки между адресами.

  • User-Agent-ом рандомизируем библиотекой, заголовки делаем реалистичными.

  • Бэкофф, Retry-After и ограничение частоты — основа долговременной работы парсера.

  • В Amvera исходящий трафик может идти через пул IP — это помогает избегать 429 без явных прокси.

  • Всё это легко собрать на Python и поддерживать в проде.

Вопрос для обсуждения: с какими подводными камнями вы чаще всего сталкивались в боевых парсерах на Python — сессии, привязанные к IP, внезапные 429, неустойчивые прокси, GraphQL-запросы вместо HTML, проблемы локали или что-то ещё? И какие библиотеки для рандомизации user-agent показали себя надёжнее у вас на проде? Опишите свой опыт в комментариях — соберём практики.

Комментарии (3)


  1. shlmzl
    26.08.2025 06:42

    Не секрет какой ИИ вам помогал в написании статьи? Я в хорошем смысле, на мой взгляд вряд ли разумно все это вводить руками, да и применять прочитанное скорее всего будут как промты. Если сразу знать какая ллм, то как бы проще стартовать.


  1. SvetlanaMikhaylova99
    26.08.2025 06:42

    А для парсинга сайта Авито такие способы подойдут?


    1. MarkovM Автор
      26.08.2025 06:42

      У меня недавна была статья именно на эту тему. Но по требованию этой доски объявлений статью забанили. Самое что смешное, в ней использовался их официальный API и не описывались спорные способы, просто запросы к официальному API. Если кратко - через их официальный API вполне реально работать, если либо ограничивать количество запросов, либо работать с нескольких IP, чтобы не превышать лимит и их правила. А вот использовать вещи описанные в этой статье лучше не стоит, у них продвинутые антибот алгоритмы, лучше работать корректно через инструменты, которые они сами дают.