Универсальный парсинг сайтов на Python: requests vs headless, токены, куки, прокси и ротация IP

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

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

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

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

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

Немного о сервисе 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). Это быстро, дёшево и легко масштабируется.

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

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

# 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-браузера никуда

Признаки:

Рекомендация — 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()

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

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

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

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

# 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",
}

Важно:

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

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

Выводы:

Удобный шаблон на 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 и бэкофф

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

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

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 со списком карточек, минуя сам скролл.

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

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

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

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

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

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

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

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

Итог

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

@MarkovM
26.08.2025 10:27 UTC
Первоисточник

Комментарии

@shlmzl
26.08.2025 06:42 UTC
+2

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

@SvetlanaMikhaylova99
26.08.2025 06:44 UTC
+1

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

@MarkovM
26.08.2025 06:55 UTC
+3

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

26.08.2025 19:56 UTC
0

А как называлась статья?

28.08.2025 08:40 UTC
0

Так и называлась "Парсинг Ави... без ограничений по запросам". Но её скрыли, к сожалению.

29.08.2025 01:40 UTC
0

Есть куча сайтов, которые копируют себе статьи с хабра сразу после их выхода в свет.

@diov
26.08.2025 13:24 UTC
0

Но как бороться с капчой Cloudflare/Yandex/... ?

@kirillkosolapov
02.09.2025 12:30 UTC
0

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

@sledov
26.08.2025 13:39 UTC
+2

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

Мне всегда казалось, что использование инструментов, заточенных под автоматизацию, таких как Selenium и Playwright, для парсинга - не самая лучшая идея. Если авторы изначально не пытались скрыть, что Playwright - это именно Playwright, а не Chrome, то попытки потом кого-то убедить в обратном превращаются в навороченную игру "найди отличия". Есть же библиотеки типа nodriver, которые управляют самим Chrome.

@chuprun
08.09.2025 17:23 UTC
0

я тоже так думал. Пока не понадобилось попросту сформировать защитные куки через js движок и потом на этом отпечатке делать api запросы. И самый простой способ в реализации оказался по сути через Playwright.
Полный реверс даже через ИИ - геммор и пустая затея, отдельное формирование js машины для рендеринга тоже в итоге. Playwright если правильно настроить, занимал на запуске за копию 2 сек, получал куки и уже был готов отправлять api запрос.

@NECR0shizo
26.08.2025 17:55 UTC
0

Недумаете, что парсинг без асинхронщины очень медленный?