Вы когда-нибудь радовались идеальному прототипу парсера, который у вас летал на демо-странице, а в проде внезапно начал ловить 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 и прочие.
Важным бонусом для нас будет являться:
-
Ротация исходящих IP из пула. Это поможет избежать большинство блокировок по IP и 429.
-
111 рублей на баланс сразу после регистрации даст пространство для тестов сразу на сервере.
-
Помимо вышесказанного, сервис предоствляет бесплатное проксирование до 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
-
Сначала «прогреваем» сессию в headless-браузере — принимаем cookies, логинимся (если нужно).
-
Сохраняем куки в
storage_state. -
Используем их уже в сессии
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 показали себя надёжнее у вас на проде? Опишите свой опыт в комментариях — соберём практики.
ссылка на оригинал статьи https://habr.com/ru/articles/940688/
Добавить комментарий