Вы когда-нибудь радовались идеальному прототипу парсера, который у вас летал на демо-странице, а в проде внезапно начал ловить 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 показали себя надёжнее у вас на проде? Опишите свой опыт в комментариях — соберём практики.
Комментарии (3)
SvetlanaMikhaylova99
26.08.2025 06:42А для парсинга сайта Авито такие способы подойдут?
MarkovM Автор
26.08.2025 06:42У меня недавна была статья именно на эту тему. Но по требованию этой доски объявлений статью забанили. Самое что смешное, в ней использовался их официальный API и не описывались спорные способы, просто запросы к официальному API. Если кратко - через их официальный API вполне реально работать, если либо ограничивать количество запросов, либо работать с нескольких IP, чтобы не превышать лимит и их правила. А вот использовать вещи описанные в этой статье лучше не стоит, у них продвинутые антибот алгоритмы, лучше работать корректно через инструменты, которые они сами дают.
shlmzl
Не секрет какой ИИ вам помогал в написании статьи? Я в хорошем смысле, на мой взгляд вряд ли разумно все это вводить руками, да и применять прочитанное скорее всего будут как промты. Если сразу знать какая ллм, то как бы проще стартовать.