Парсинг данных без блокировок: как собирать информацию эффективно и безопасно

Парсинг и данные
Схема слоёв анти-бот защиты: IP-адрес, TLS-отпечаток, HTTP-заголовки, браузерное окружение и поведение пользователя

TL;DR: Блокировка при парсинге — это почти всегда диагноз конкретного слоя: IP, TLS-отпечаток, заголовки, браузерное окружение или поведение. Чинить нужно тот слой, который палится, а не менять всё подряд. База: уважать robots.txt и лимиты ресурса, парсить в разумном темпе, кэшировать. Техника: резидентские прокси, curl_cffi с impersonate вместо голого requests, консистентные заголовки, Playwright там, где без JS не обойтись, и инфраструктура с очередями, backoff и хранением сырого HTML.

Как устроена блокировка: слои, а не одна стена

Анти-бот системы (Cloudflare, Datadome, PerimeterX и другие) не принимают решение по одному признаку. Они собирают сигналы на нескольких уровнях и складывают их в скор. Превысили порог — получили 403 или капчу. Поэтому полезно думать о защите слоями и понимать, на каком именно вас поймали:

  1. IP-адрес — репутация адреса, его принадлежность датацентру или провайдеру, частота запросов с него.
  2. TLS-отпечаток — JA3/JA4: какие шифры и расширения ваш клиент предлагает при TLS-рукопожатии.
  3. HTTP-заголовки — набор, значения и их порядок.
  4. Браузерное окружение — выполняется ли JavaScript, какие признаки выдаёт headless-браузер.
  5. Поведение — темп запросов, паттерны навигации, движения мыши, скроллинг.

Классическая ошибка — чинить не тот слой. Крутите User-Agent, а бан идёт по TLS-отпечатку. Меняете прокси, а выдаёт паттерн поведения. Дальше разберём каждый слой отдельно.

Слой 1: IP-адрес

Если с одного IP идут сотни запросов в минуту, никакие хитрые заголовки не спасут — адрес улетит в бан по rate-limit.

Что работает

  • Резидентские прокси — IP реальных домашних пользователей. Высокая репутация, подходят для сайтов с серьёзной защитой. Минус — цена и скорость.
  • ISP-прокси — адреса, зарегистрированные на интернет-провайдеров, но размещённые в датацентрах. Средний вариант по цене и репутации, часто лучший компромисс для долгих сессий.
  • Мобильные прокси — IP мобильных операторов. За одним адресом у оператора сидят тысячи абонентов (CGNAT), поэтому банить такой IP защитным системам невыгодно. Хороши для самых агрессивных целей, но дороги и медленны.
  • Датацентровые прокси — дёшево и быстро, но подсети датацентров известны всем анти-бот системам. Годятся только для целей со слабой защитой.

Цен и цифр по провайдерам сознательно не привожу: рынок меняется быстрее, чем устаревает статья, и сравнивать нужно под вашу задачу — гео, объём, требования к сессиям.

Ротация и лимиты

Ротация IP решает половину проблемы. Вторая половина — не превышать разумную частоту запросов даже с «чистого» адреса. Я обычно стартую с 1 запроса в 2–5 секунд на IP и смотрю на долю ошибок 403/429. Если она растёт — снижаю темп, а не увеличиваю пул.

Минимальный пример прокси на httpx (в актуальных версиях параметр называется proxy, а не proxies):

import httpx

with httpx.Client(
    proxy="http://user:pass@proxy_host:port",
    timeout=15.0,
    follow_redirects=True,
) as client:
    response = client.get("https://example.com")
    print(response.status_code)

Есть и неочевидный момент: старайтесь держать сессию (куки, авторизация) на одном IP. Резкая смена адреса посреди сессии — сам по себе сигнал для защиты.

Слой 2: TLS-отпечаток (JA3/JA4)

Это тот слой, про который забывают чаще всего. При TLS-рукопожатии клиент отправляет ClientHello: список шифров, расширений, версий TLS, эллиптических кривых. Из этого набора строится отпечаток — JA3, а сейчас всё чаще JA4. У Python-библиотеки requests этот отпечаток стабильно отличается от Chrome и Firefox. Защите достаточно сравнить JA3 с заявленным User-Agent: говорите, что вы Chrome, а рукопожатие — от Python. Бан без единого запроса к контенту.

Решение: curl_cffi с impersonate

Библиотека curl_cffi построена на curl-impersonate и умеет воспроизводить TLS-отпечаток реального браузера:

from curl_cffi import requests

response = requests.get(
    "https://example.com",
    impersonate="chrome",
    proxy="http://user:pass@proxy_host:port",
    timeout=15,
)
print(response.status_code)

# Сессия с сохранением кукисов
session = requests.Session(impersonate="chrome")
page1 = session.get("https://example.com/catalog")
page2 = session.get("https://example.com/catalog/item/123")

Значение impersonate можно указывать точнее — например, конкретную мажорную версию браузера, которую поддерживает установленная версия curl_cffi. Важно: обновляйте библиотеку, потому что отпечатки старых версий браузеров со временем сами становятся подозрительными.

Моя позиция: для HTTP-парсинга (без рендеринга JS) curl_cffi сегодня — базовый выбор, а не requests. Переписывать с requests на curl_cffi обычно занимает минуты — API намеренно совместим, — а эффект на защищённых сайтах заметен сразу.

Слой 3: заголовки и их порядок

Здесь главное слово — консистентность. Защита смотрит не только на значения, но и на то, что заголовки не противоречат друг другу и TLS-отпечатку.

Главная ошибка старых гайдов — совет «всегда ставьте актуальный User-Agent» с примером вроде Chrome 108. Во-первых, конкретная версия из примера давно снята с поддержки и сама по себе выдаёт бота. Во-вторых, UA не должен жить отдельно от остального: если User-Agent говорит «Chrome на Windows», а TLS-отпечаток — от Python, а в заголовках нет sec-ch-ua и sec-ch-ua-platform, которые Chrome всегда отправляет, — вы подаете три противоречивых сигнала сразу.

Практические правила:

  • UA должен соответствовать выбранному impersonate в curl_cffi и платформе прокси. Единый «персонаж»: один браузер, одна ОС, одна локаль.
  • Порядок заголовков имеет значение. Браузеры отправляют их в стабильном порядке, а requests — в порядке dict. curl_cffi при impersonate воспроизводит и порядок браузера, ещё один аргумент в его пользу.
  • Referer должен быть осмысленным: при переходе по внутренней ссылке — предыдущая страница того же сайта, а не всегда Google.
  • Не забивайте заголовки в код вручную по списку из гайдов пятилетней давности. Откройте DevTools реального браузера, скопируйте реальный запрос и сравните со своим.

Слой 4: браузерное окружение

Когда контент рендерится JavaScript или защита проверяет JS-окружение (canvas, WebGL, navigator), чистого HTTP недостаточно. Нужен настоящий браузер — Playwright.

Но headless-браузер «из коробки» заметен: navigator.webdriver равен true, нет плагинов, странное разрешение, упрощённый WebGL. Минимальная разумная конфигурация:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=True,
        args=["--disable-blink-features=AutomationControlled"],
    )
    context = browser.new_context(
        locale="ru-RU",
        viewport={"width": 1920, "height": 1080},
        timezone_id="Europe/Moscow",
    )
    # Маскируем главный маркер автоматизации
    context.add_init_script(
        "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
    )
    page = context.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")
    page.wait_for_timeout(2500)  # даём JS отработать
    html = page.content()
    browser.close()

Нюансы, которые часто упускают:

  • Часовой пояс и язык должны совпадать с геолокацией прокси. IP из Германии, timezone_id="Europe/Moscow" и locale="ru-RU" — расхождение, которое видно без всякого везения.
  • Headless-режим сам по себе палится по ряду признаков. Если цель важная, иногда надёжнее запускать браузер с интерфейсом под виртуальным дисплеем, чем городить сложную маскировку headless.
  • Браузер дорогой: сотни миллисекунд на HTTP-запрос против 3–10 секунд на страницу с рендерингом. Поэтому разумная архитектура — curl_cffi для всего, что отдаётся без JS, и Playwright только для страниц, где он действительно нужен. Заметную часть «нужен браузер» на практике можно заменить прямыми вызовами внутренних JSON-API сайта — смотрите вкладку Network в DevTools.

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

Слой 5: поведение

Даже идеальный запрос выдаст бота, если таких запросов тысяча в минуту и все идут строго по прямой: главная → товар → следующий товар.

Что учитывать:

  • Случайные задержки, а не фиксированные. Человек не кликает ровно раз в 3 секунды. Плюс редкие длинные паузы — «ушёл за кофе».
  • Реалистичная навигация: заход на главную или категорию перед страницей цели, а не только прямые заходы на глубокие URL.
  • Суточный ритм: круглосуточная равномерная нагрузка с «человеческих» резидентских IP выглядит странно. Пики днём, провалы ночью — естественнее.
  • Объём на сессию: живой пользователь не просматривает 5000 карточек товара подряд с одной сессии.

Если собираете данные для обучения моделей, поведенческий слой важен вдвойне: объёмы большие, паттерн заметнее. Об особенностях сбора обучающих данных и хранения больших корпусов я писал в статье про память AI-систем — там же про то, почему сырые данные стоит сохранять до обработки.

CAPTCHA: индикатор, а не цель

Если вы стабильно получаете капчу — защита уже сложила достаточный скор на нижних слоях. Сервисы решения капч существуют, но делать их основным путём я не рекомендую по двум причинам. Первая — репутационная и юридическая: систематический обход явных мер защиты — это уже не «сбор открытых данных», и к вопросу нужно относиться как к риску, а не как к строчке в бюджете. Вторая — инженерная: решение капчи стоит денег и секунд на каждый запрос, и экономика парсинга быстро перестаёт сходиться.

Правильная реакция на капчу — отступить и пересмотреть слои: снизить темп, сменить тип прокси, проверить консистентность отпечатков, увеличить паузы. Гораздо чаще капча лечится этим, чем подключением распознавания.

А до капчи вообще стоит сделать шаг назад:

  • Проверьте robots.txt и условия использования ресурса. Если данные явно закрыты от сбора — возможно, задачу нужно решать иначе.
  • Поищите официальный API. Многие площадки отдают данные легально, дёшево и без игр в кошки-мышки.
  • Кэшируйте. Если страница обновляется раз в сутки, незачем дёргать её каждый час — вы снижаете и нагрузку на чужой ресурс, и свой профиль риска.

Коротко о юридической рамке (не юридическая консультация)

По российскому праву сбор публичных данных сам по себе не запрещён, но есть границы. Статья 1260 и 1334 ГК РФ защищают базы данных как объекты интеллектуальной собственности: извлечение существенной части базы и её использование могут нарушать права изготовителя. Персональные данные регулируются 152-ФЗ — собирать, хранить и использовать их без законного основания нельзя, даже если они лежат в открытом доступе. Плюс условия использования конкретного сайта и запрет на создание чрезмерной нагрузки. Вывод без юридических советов: публичные неперсональные данные, разумный темп, уважение robots.txt и отсутствие обхода явных технических барьеров — самая безопасная зона. Для коммерческого проекта с сомнительными границами — поговорите с юристом до запуска, а не после претензии.

Инфраструктура: почему «скрипт в цикле» не доживает до продакшна

Техническая сторона парсинга на объёмах — это уже распределённая система, и относиться к ней нужно соответственно.

Очереди и повторы

Задачи (URL) складываются в очередь — Redis, RabbitMQ, что угодно знакомое. Воркеры берут задачи и обрабатывают. Ошибки (429, 403, таймауты) не теряются, а возвращаются в очередь с экспоненциальным backoff:

import random
import time

def fetch_with_retry(fetch_fn, url, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return fetch_fn(url)
        except Exception:
            if attempt == max_attempts - 1:
                raise
            delay = min(2 ** attempt + random.uniform(0, 1), 60)
            time.sleep(delay)

Backoff важен вдвойне, если сайт уже начал вас душить: агрессивные повторы только усугубляют бан.

Идемпотентность и дедупликация

Любая задача в очереди может быть выполнена дважды: ретрай, падение воркера, повторная постановка. Запись результата должна быть идемпотентной — upsert по стабильному ключу (URL или ID сущности на сайте), а не insert. Дедупликация URL до постановки в очередь — через множество в Redis или unique-индекс в базе. На больших списках до 30–40% URL оказываются дублями, и каждый дубль — лишний запрос и лишний риск.

Хранение сырого HTML

Сохраняйте сырой ответ (HTML/JSON) до разбора. Причины две. Первая: парсер меняется — сайт обновил вёрстку, вы поправили селекторы, и со сырым HTML переразбор делается без новых запросов к сайту, то есть бесплатно и без риска бана. Вторая: сырой ответ — единственный надёжный источник при отладке «почему поля пустые». Сжатый HTML в объектном хранилище стоит копейки относительно стоимости повторного обхода. Этот же подход — «сырьё сначала, обработка потом» — логично стыкуется с пайплайнами для последующей обработки документов, например RAG-пайплайном для корпоративных документов, а если в собранных страницах сканы — пригодится разбор современного OCR.

Мониторинг

Минимум для эксплуатации: доля успешных ответов по времени, распределение статус-кодов, доля 403/429 по группам прокси, скорость обхода, глубина очереди. Алерт на рост доли ошибок — первый сигнал, что защита адаптировалась и пора чинить конкретный слой, пока не полег весь пул.

FAQ

С какого слоя начинать, если парсер заблокировали?

С диагностики. Получаете 403 сразу, до контента — вероятнее TLS-отпечаток или IP. 403 после нескольких успешных страниц — темп и поведение. Капча — накопленный скор по всем слоям. Таймауты с конкретных прокси — репутация адресов. Не меняйте всё сразу, иначе не поймёте, что сработало.

Нужен ли Playwright всегда?

Нет, и без необходимости использовать его не стоит: он в разы медленнее и дороже по ресурсам. Сначала проверьте, отдаёт ли сайт данные в HTML или через внутренние JSON-эндпоинты — вкладка Network в DevTools. Значительная часть «динамических» сайтов парсится чистым HTTP через curl_cffi.

Сколько прокси нужно?

Зависит от объёма и защищённости цели. Для мониторинга пары сотен страниц в день хватит нескольких адресов. Для обхода каталога в сотни тысяч страниц — нормальный пул резидентских или ISP с ротацией. Начните с малого пула и консервативного темпа и масштабируйте по метрике доли ошибок, а не наугад.

Легально ли это вообще?

Сбор публичных данных при разумной нагрузке, соблюдении robots.txt и без персональных данных — самая безопасная зона. Риски появляются при извлечении существенных частей баз данных, обходе явных барьеров и работе с персональными данными. Для коммерческого проекта границы лучше обсудить с юристом заранее.

Стоит ли брать бесплатные прокси?

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

Заключение

Парсинг без блокировок — это не «список из десяти прокси-советов», а работа по слоям: диагностика, какой слой палится, точечное исправление, метрики успеха. Базовый набор на сегодня: curl_cffi с impersonate вместо requests, консистентные заголовки и UA под выбранный отпечаток, резидентские или ISP-прокси с умеренным темпом, Playwright только там, где без рендеринга не обойтись, и инфраструктура с очередями, backoff, дедупликацией и хранением сырого HTML. И прежде всего — уважение к ресурсу: лимиты, robots.txt, официальные API. Это дешевле и надёжнее любой капчи-фермы.

Нужен стабильный парсинг для вашего AI-продукта — от сбора данных до их обработки и хранения? Напишите мне — обсудим вашу задачу.

Обсудить проект

Есть идея или задача? Давайте обсудим, как можно её реализовать с помощью современных AI-технологий.

Написать мне
Вернуться к блогу