Skip to content

База знаний Data Ox

В этой базе данных собраны:

  • теоретические аспекты скрапинга (как работает скрапинг, какие бывают проблемы, какие виды авторизации существуют и т.д.)
  • способы обхода защиты (jitter, подмена SSL, fingerprinting, антибот-системы и т.д.)
  • способы чтения данных (парсинг, JSON, OCR, через LLM и т.д.)
  • способы скрапинга (через API, через браузер, через Android приложение, через десктоп приложение и т.д.)

База знаний ориентирована на Python. Исключение: расширения для браузеров, Android приложение и десктоп приложение.


Введение

Скрапинг — это когда мы получаем данные с сайта или приложения. Например, товары с ценами, котировки акций, профили пользователей, новости и т.д.

Для примера, данные заказывают, чтобы:

  • мониторить цены конкурентов (e-commerce, маркетплейсы)
  • собирать лиды и контакты для продаж
  • агрегировать вакансии с сайтов по трудоустройству
  • анализировать отзывы и рейтинги продуктов
  • отслеживать новости и упоминания бренда
  • получать финансовые данные для трейдинга: котировки, криптовалюта, биржевые цены
  • строить датасеты для машинного обучения
Скрапинг примерСкрапинг пример

Как обычно делается скрапинг

Скрапинг — это набор подходов, каждый из которых подходит под конкретную задачу. Каждый раз начинаем с самого простого и переходим к более сложным.


Простые способы скрапинга

  • API запрос — делаем GET website.com/api/product/1 и получаем готовый JSON. Самый предпочтительный вариант, если API есть или его можно найти.
  • Парсинг статичного HTML — делаем запрос к странице (website.com/product/1) и парсим HTML. Иногда используем посредников вроде scraperapi.com, которые сами обходят базовые защиты и возвращают HTML.
  • Headless браузер — рендерим страницу через Playwright и парсим HTML. Нужен, когда контент генерируется JavaScript'ом или не получается достать данные из API.

Более сложные способы скрапинга

  • Через платный антидетект-браузер (Multilogin, NST, GoLogin)
  • Через браузерное расширение в реальном браузере
  • Через Android приложение (в эмуляторе или на реальном устройстве)
  • Через десктопное приложение

Правило 80/20: в 80% случаев достаточно API запросов, парсинга статичного HTML или headless браузера. Это самые простые и хорошо масштабируемые способы — их и нужно пробовать в первую очередь.

Более сложные методы (Android, десктоп, расширения) используем только тогда, когда всё остальное не сработало.

Если объём скрапинга большой — сразу целимся в API, даже если его сложно найти. Скрапить десятки тысяч товаров на регулярной основе через Android приложение или браузерное расширение очень-очень проблематично.

Так что иногда резонно потратить много времени на поиск API, даже если нужно сниффить Android приложение или расковыривать сложный механизм получения токена авторизации.


Какие бывают проблемы при скрапинге?

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


Авторизация

Instagram авторизация

Большинство сайтов требуют аккаунт, чтобы показать контент или разрешить делать API запросы.

Примеры:

  1. Мобильное приложение использует API с токеном авторизации, который живёт 1 час. Значит, нам нужно автоматически обновлять токен через само приложение и подставлять его в API запросы. Без этого через час скрапер сломается.

  2. Сессия в браузере может истечь, и нужно заново проходить авторизацию через Playwright.

  3. Чтобы делать скрапинг LinkedIn, нам нужно закупать аккаунты или регистрировать самостоятельно и делать "прогрев" вручную.


Капча

Google капча

Cloudflare и другие провайдеры DDOS защиты умеют отличать ботов от людей по поведению, fingerprint'у браузера, TLS-сигнатуре и десяткам других параметров. В случае подозрения показывается капча, чтобы убедиться, что это не робот.

Основные варианты решения:

  • Использовать посредников (scraperapi.com, BrightData) — они берут это на себя
  • Настроить правильные заголовки, User-Agent и TLS fingerprint
  • Рендерить через браузер с undetectable-патчами
  • Использовать прокси

Блокировка IP

Блокировка IP

Даже если обошли Cloudflare, сайт будет блокировать IP по частоте запросов, если скрапим быстро или с одного адреса.

Простой requests.get() в Python получает 403 код ошибки.

403 код ошибка, он же "Forbidden", означает, что доступ запрещён.

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


Какие виды авторизации существуют?

Сессии — сервер хранит состояние авторизации у себя, клиент только передаёт ID сессии. Обычно хранится в куки. Нужно поддерживать session объект в requests, чтобы куки автоматически передавались с каждым запросом.

Токены (JWT и аналоги) — сервер выдаёт токен при логине, клиент передаёт его в заголовке Authorization: Bearer <token>. Токены имеют срок жизни, поэтому нужен механизм обновления (refresh token или повторный логин).

Куки — иногда авторизация завязана именно на куки, а не на сессии в классическом смысле. Нужно правильно их передавать и следить за сроком жизни.

На практике сайты часто комбинируют несколько механизмов. Например, авторизация через POST /login возвращает куки с сессией, а параллельно устанавливает CSRF-токен, который нужно передавать в каждом последующем запросе.











Общие советы

Определение масштабов скрапинга

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

Грубая классификация:

  • Разово, несколько тысяч страниц — можно обойтись простым скриптом без лишней инфраструктуры или полуручным скрапингом через браузерное расширение
  • Регулярно, десятки тысяч страниц — уже нужно думать про прокси, retry-логику, мониторинг
  • Сотни тысяч и больше, на регулярной основе — без API не обойтись. Браузерный скрапинг в таких объёмах слишком медленный и нестабильный

Разделение на скрапинг и парсинг

Если скрапим большой объем данных, нужно разделить на скрапинг и парсинг.

Например, сначала сохраняем HTML страниц с товарами (допустим, 100 000 страниц) в базу данных или на диск. Затем парсим HTML в те данные, которые запрашивает клиент. Если скрапим одни и те же страницы раз в какой-то период, можно делать версионированние (с указанием даты скрапинга).

Зачем:

  • иногда мы не знаем все поля, которые нужны;
  • на половине пути сайт обновит верстку и окажется, что мы половину распарили не так;
  • может быть нужно проанализировать изменения за период.

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

Для исходников можно использовать blob\TEXT в PostgreSQL (удобно делать версионирование).

python
import uuid
import requests
from bs4 import BeautifulSoup
from datetime import datetime

...

def scrape(url: str, product_id: str, repo: RawPageRepository):
    html = requests.get(url).text
    repo.save(
        id=str(uuid.uuid4()), # using unique ID as we have different versions of the page
        product_id=product_id,
        html=html,
        scraped_at=datetime.now(timezone.utc), # using UTC to avoid timezone issues
    )

def parse(product_id: str, repo: RawPageRepository) -> dict:
    page = repo.get_latest(product_id)
    soup = BeautifulSoup(page.html, "html.parser")
    return {
        "price": soup.select_one(".price").text,
        "title": soup.select_one("h1").text,
        "scraped_at": page.scraped_at,
    }

Таблица raw_pages хранит все версии по дате — можно сравнивать цены за любой период без повторного скрапинга.


Валидация данных и healthchecks

После парсинга нужно проверять, что данные выглядят адекватно. Скрапер может "успешно" завершиться, но при этом вернуть пустые поля — потому что сайт поменял вёрстку, и селектор больше ничего не находит. Или делать запросы, но не получать данные.

Если мы скрапим на регулярной основе, удобно делать healthcheck эндпоинт, который возвращает 400 ошибку, если данные невалидны или давно не поступали.

python
last_scraped_at = datetime.now()
is_data_valid = True

app.get('/healthcheck')
def is_healthy():
    if datetime.now() - last_scraped_at > timedelta(minutes=60):
        # данные не обновлялись более 60 минут
        return jsonify({'error': 'Last scraped more than 60 minutes ago'}), 400

    if not is_data_valid:
        # данные невалидны
        return jsonify({'error': 'Data is not valid'}), 400

    return jsonify({'message': 'Data is valid'}), 200

while True:
    scrapped_items = scrape_data()

    for item in scrapped_items:
        if not item.price:
            is_data_valid = False
            break

        if not item.title:
            is_data_valid = False
            break

    if len(scrapped_items) > 0:
        last_scraped_at = datetime.now()

Логирование

Если скрапер долго работает на сервере и почему-то перестал — нужны логи. Логи пишутся одновременно и в консоль, и в хранилище логов. В хранилище логов удаленно можно посмотреть, что сломалось (страница, аккаунт, прокси). Для логов используем VictoriaLogs и структурированные логи.

Пример структурированного логгирования:

python
log = get_logger(account_id="acc_123", proxy_ip="1.2.3.4")

for page in range(1, total_pages + 1):
    log.info(
        "Scraping page",
        extra={"extra": {"page": page, "url": f"https://example.com/items?page={page}"}},
    )

    try:
        html = scraper.fetch_page(page)
    except Exception as e:
        log.error(
            "Failed to fetch page",
            extra={"extra": {"page": page, "error": str(e)}},
        )
        continue

    log.info("Page fetched successfully", extra={"extra": {"page": page}})

    items = parser.parse(html)

    log.info("Page parsed", extra={"extra": {"page": page, "items_found": len(items)}})

Окно VictoriaLogs:

VictoriaLogs

Можем фильтровать:

  • по IP прокси
  • по ID аккаунта
  • по интервалу времени

Почему VictoriaLogs?

  • Разворачивается на сервере тремя строками кода в Docker Compose вместе с UI
  • Очень сильно сжимает логи (~16 раз) и потребляет очень мало ресурсов (до ~50-100 Мб RAM)

Скрапинг через API с генерацией токена через браузер

Частый сценарий: API существует, но токен для него нельзя (или очень сложно) получить напрямую. Нужно открыть браузер, пройти авторизацию (возможно с капчей или 2FA) и вытащить токен из куков или localStorage. И это нужно делать регулярно.

Подходы:

  • Открываем браузер, логинимся, вытаскиваем токен и дальше вызываем API
  • Через оператора — показываем интерфейс человеку раз в какое-то время, он вручную логинится и передаем нам токен

Запуск из-под Windows при деплое

Если используем не headless-браузер (и не имитируем user-agent), при деплое на Linux всё может сломаться.

В этом случае пробуем задеплоить на Windows. Часто помогает, потому что защиты опасаются Linux компьютеров (и не зря).


Что делать, если ничего не вышло?

Иногда сайт защищён настолько хорошо, что соскрапить его быстро не получится. В этом случае:

  • Ищем данные на другом сайте — часто те же данные есть на другом, менее защищённом сайте (недвижимость, биржи, резюме);
  • Покупаем данные — иногда данные уже кто-то собрал и продаёт (особенно актуально в финансах)

Как прогревать аккаунты?

Многие платформы (LinkedIn, Instagram, Twitter и другие) блокируют аккаунты, которые сразу начинают вести себя как боты. Прогрев — это постепенное "очеловечивание" аккаунта перед тем, как начать скрапинг.

Зачем прогревать:

  • Новый аккаунт без истории вызывает подозрение
  • Резкие изменения в поведении (например, 500 запросов в первый день) — красный флаг
  • Прогретый аккаунт живёт дольше и реже попадает под ограничения

Общий принцип прогрева:

  1. Первые несколько дней — только пассивное поведение: листаем ленту, смотрим профили, не делаем никаких массовых действий
  2. Потом постепенно — добавляем лайки, подписки, комментарии. Начинаем с малого и наращиваем активность
  3. Только после этого — начинаем делать целевые действия (скрапинг, парсинг, массовые запросы)

Конкретные советы:

  • Используем прокси из того же региона, что и предполагаемый "владелец" аккаунта — смена IP каждый день подозрительна
  • Имитируем реалистичное расписание активности: активен днём, не активен ночью
  • Не выполняем одни и те же действия в строгом порядке — добавляем вариативность
  • Не превышаем лимиты платформы (у каждой свои). Для LinkedIn, например, не более 100 запросов в день в первые недели
  • Заполняем профиль полностью: фото, описание, контакты. Пустые профили вызывают больше подозрений
  • Для массового скрапинга держим пул аккаунтов, чтобы распределять нагрузку











Обход защит

ML модели для детектирования ботов

Под капотом DDOS\антибот защиты используют ML модели для определения ботов. Поэтому они могут анализировать поведение динамически, подстраиваться под обход защит и анализировать закономерности в сотнях параметров (о чем ниже) в режиме реального времени. Так что помним, что обход скрапинга — это постоянное "противостояние меча и щита".


Fingerprinting и как его рандомизировать

Блокировщики ботов и сами сайты собирают "отпечаток" браузера — совокупность десятков параметров, которые уникально идентифицируют человека. Даже при смене IP и User-Agent, одинаковый fingerprint выдаёт бота.

Что входит в fingerprint:

  • Navigator: userAgent, platform, language, hardwareConcurrency, deviceMemory, plugins, mimeTypes
  • Screen: разрешение, colorDepth, pixelRatio, availWidth/availHeight
  • Canvas fingerprint: сайт рендерит скрытый <canvas> с текстом и формами, снимает пиксели через toDataURL() — рендеринг отличается на разных GPU, драйверах и ОС
  • WebGL: характеристики видеокарты (RENDERER, VENDOR), поддерживаемые расширения
  • AudioContext: шум при обработке аудиосигнала тоже уникален для железа
  • Шрифты: через Canvas или document.fonts определяют, какие шрифты установлены в системе
  • Timezone и locale: Intl.DateTimeFormat().resolvedOptions(), Date.getTimezoneOffset()
  • Поведенческие паттерны: движение мыши, ритм нажатий клавиш (keystroke dynamics), скорость скролла

Инструменты рандомизации:

  • playwright-stealth — набор патчей, которые перехватывают JS-вызовы и подменяют значения. Патчит navigator.webdriver, Canvas, WebGL, navigator.plugins и другие утечки. Пример:
python
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    stealth_sync(page)
    page.goto("https://example.com")
  • Антидетект-браузеры — платные Chromium-форки с патчами на уровне движка, а не JS-хуков. Обычно используем: Multilogin, GoLogin, NST Browser. Каждый профиль имеет изолированное хранилище, уникальные Canvas/WebGL-значения и согласованный набор параметров (язык, таймзона, UA, разрешение — всё соответствует друг другу)

TLS Fingerprinting

При установке HTTPS-соединения клиент отправляет ClientHello — пакет с перечнем поддерживаемых алгоритмов шифрования, TLS-расширений и алгоритмов подписи. Эта комбинация называется JA3-сигнатурой и уникальна для каждой клиентской библиотеки. requests и aiohttp используют OpenSSL с фиксированным набором параметров, про который знают антибот-системы.

Как проверить свою JA3:

Открыть https://tls.browserleaks.com/json из браузера и из скрипта — JA3-хэши будут разными.

JA3

Решения:

  • curl-cffi — Python-обёртка над curl-impersonate. Имитирует TLS-стек конкретных версий Chrome, Firefox или Safari на уровне C-библиотеки. Самый простой вариант для HTTP-скрапинга:
python
from curl_cffi import requests

# impersonate задаёт и TLS-сигнатуру, и HTTP/2 SETTINGS frame, и заголовки
response = requests.get(
    "https://example.com",
    impersonate="chrome124",  # chrome110, chrome124, firefox117, safari17_0 и др.
)
  • tls-client — аналог, обёртка над Go-библиотекой utls. Поддерживает те же профили:
python
import tls_client

session = tls_client.Session(
    client_identifier="chrome_124",
    random_tls_extension_order=True  # дополнительная рандомизация порядка расширений
)
response = session.get("https://example.com")
  • httpx с hishel или кастомным transport — менее удобно, требует ручной настройки ssl.SSLContext и не даёт полноценной имитации HTTP/2 SETTINGS frame. Подходит только если JA3 не является проблемой, а нужен просто async.

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


Заголовки и их правильная настройка

Антибот-системы анализируют не только наличие заголовков, но и их порядок, регистр и взаимную согласованность. HTTP/2 передаёт заголовки в бинарном формате HPACK — порядок полей фиксирован у конкретного браузера и является частью fingerprint.

Минимально необходимый набор для имитации Chrome:

python
headers = {
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "accept-encoding": "gzip, deflate, br, zstd",
    "accept-language": "en-US,en;q=0.9",
    "cache-control": "max-age=0",
    "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": '"Windows"',
    "sec-fetch-dest": "document",
    "sec-fetch-mode": "navigate",
    "sec-fetch-site": "none",
    "sec-fetch-user": "?1",
    "upgrade-insecure-requests": "1",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
}

Правила согласованности:

  • sec-ch-ua-platform должен совпадать с платформой в user-agent (оба Windows или оба macOS)
  • sec-ch-ua версия должна совпадать с версией Chrome в user-agent
  • При переходе по ссылке на сайте должен присутствовать referer с предыдущей страницы
  • sec-fetch-site меняется в зависимости от контекста: none для прямого перехода, same-origin для запросов внутри сайта, cross-site для запросов к внешним ресурсам

Как получить актуальный набор заголовков:

  1. Открыть DevTools (F12) → вкладка Network
  2. Открыть нужную страницу или сделать нужный запрос
  3. Правый клик на запросе → Copy → Copy as cURL
  4. Вставить в https://curlconverter.com — получится готовый Python-код с заголовками
Headers

Для XHR/fetch-запросов (API внутри сайта) набор заголовков отличается — там нет sec-fetch-dest: document, вместо этого будет sec-fetch-dest: empty и sec-fetch-mode: cors. Копировать нужно именно тот запрос, который хочется воспроизвести.


Задержки между запросами (jitter)

Антибот-системы строят статистику запросов по IP: слишком равномерный интервал — явный признак автоматизации. Реальный пользователь читает страницу, отвлекается, думает — его тайминги подчиняются логнормальному распределению, а не равномерному.

python
import math
import random
import time

def human_sleep(mean=2.0, sigma=0.5):
    # Логнормальное распределение: большинство пауз около mean, но иногда намного дольше
    delay = random.lognormvariate(math.log(mean), sigma)
    delay = max(0.5, min(delay, 15.0))  # ограничить разумными пределами
    time.sleep(delay)

Паузы после ключевых действий — после загрузки страницы стоит подождать дольше, имитируя чтение:

python
import random
import time

def reading_pause(content_length: int):
    # ~200 слов в минуту, примерно 5 символов на слово
    words = content_length / 5
    reading_time = words / 200 * 60  # в секундах
    # Читаем не всё, добавляем разброс
    actual = reading_time * random.uniform(0.1, 0.4)
    time.sleep(max(1.0, min(actual, 20.0)))

Учёт "рабочего времени" — если целевой сайт работает в определённом регионе, массовый скрапинг ночью по местному времени аномален:

python
from datetime import datetime
import pytz
import time
import random

def rate_by_hour(target_tz="America/New_York") -> float:
    """Возвращает множитель задержки в зависимости от времени суток."""
    tz = pytz.timezone(target_tz)
    hour = datetime.now(tz).hour
    if 9 <= hour <= 18:
        return 1.0   # рабочее время — нормальный темп
    elif 19 <= hour <= 22:
        return 1.5   # вечер — чуть медленнее
    else:
        return 4.0   # ночь — очень медленно или пауза

base_delay = 2.0
time.sleep(base_delay * rate_by_hour() * random.uniform(0.8, 1.2))

Ограничение RPM (requests per minute) — иногда удобнее задать верхний предел, а не абсолютные задержки:

python
import time

class RateLimiter:
    def __init__(self, max_rpm: int):
        self.min_interval = 60.0 / max_rpm
        self._last = 0.0

    def wait(self):
        elapsed = time.monotonic() - self._last
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed + random.uniform(0, 0.3))
        self._last = time.monotonic()

limiter = RateLimiter(max_rpm=20)
for url in urls:
    limiter.wait()
    response = session.get(url)

Имитация человеческого поведения в браузере (headless, платный и т.д.)

Некоторые антибот-системы собирают поведенческие события прямо на странице через JS-сенсоры: координаты мыши, скорость движения, нажатия клавиш, события скролла. Прямой page.click(selector) в Playwright даёт клик с идеальной точностью и нулевым временем движения — это аномалия.

Реалистичное движение мыши (кривая Безье):

python
import asyncio
import random
import math

async def human_move(page, target_x: float, target_y: float, steps: int = 25):
    """Движение мыши по кривой Безье с вариацией скорости."""
    current = await page.evaluate("() => ({ x: window.mouseX || 0, y: window.mouseY || 0 })")
    start_x, start_y = current.get("x", 100), current.get("y", 100)

    # Контрольная точка для кривой (случайное отклонение)
    cp_x = (start_x + target_x) / 2 + random.randint(-100, 100)
    cp_y = (start_y + target_y) / 2 + random.randint(-80, 80)

    for i in range(steps + 1):
        t = i / steps
        # Квадратичная кривая Безье
        x = (1 - t) ** 2 * start_x + 2 * (1 - t) * t * cp_x + t ** 2 * target_x
        y = (1 - t) ** 2 * start_y + 2 * (1 - t) * t * cp_y + t ** 2 * target_y
        await page.mouse.move(x, y)
        # Скорость в начале и конце ниже (ease-in-out)
        speed = 0.5 + math.sin(t * math.pi) * 0.5
        await asyncio.sleep(random.uniform(0.008, 0.025) / speed)

    await page.mouse.move(target_x, target_y)

async def human_click(page, selector: str):
    element = await page.query_selector(selector)
    box = await element.bounding_box()
    # Кликать не точно в центр, а со случайным смещением внутри элемента
    x = box["x"] + box["width"] * random.uniform(0.3, 0.7)
    y = box["y"] + box["height"] * random.uniform(0.3, 0.7)
    await human_move(page, x, y)
    await asyncio.sleep(random.uniform(0.05, 0.15))
    await page.mouse.click(x, y)

Реалистичный скролл:

python
async def human_scroll(page, distance: int = None):
    """Скролл с ускорением и замедлением, несколькими этапами."""
    if distance is None:
        distance = random.randint(300, 800)

    viewport_height = await page.evaluate("window.innerHeight")
    scrolled = 0

    while scrolled < distance:
        chunk = random.randint(80, 200)
        await page.mouse.wheel(0, chunk)
        scrolled += chunk
        await asyncio.sleep(random.uniform(0.05, 0.15))

    # Иногда немного проскроллить назад
    if random.random() < 0.3:
        back = random.randint(30, 100)
        await page.mouse.wheel(0, -back)
        await asyncio.sleep(random.uniform(0.2, 0.5))

Реалистичный ввод текста (для форм):

python
async def human_type(page, selector: str, text: str):
    await human_click(page, selector)
    await asyncio.sleep(random.uniform(0.2, 0.5))

    for char in text:
        await page.keyboard.type(char)
        # Скорость печати варьируется: 50-200 мс между символами
        delay = random.gauss(100, 30)  # ~100 мс среднее, нормальное распределение
        delay = max(40, min(delay, 300))
        await asyncio.sleep(delay / 1000)

    # Иногда делать опечатку и исправлять
    if random.random() < 0.1 and len(text) > 3:
        await page.keyboard.press("Backspace")
        await asyncio.sleep(random.uniform(0.3, 0.8))
        await page.keyboard.type(text[-1])

Сценарий "осмотра" страницы перед целевым действием:

python
async def browse_naturally(page, target_selector: str):
    """Имитирует чтение страницы перед тем, как кликнуть на нужный элемент."""
    # Подождать загрузки
    await asyncio.sleep(random.uniform(1.5, 3.0))

    # Проскроллить вниз, как будто читаем
    await human_scroll(page, random.randint(400, 900))
    await asyncio.sleep(random.uniform(1.0, 2.5))

    # Проскроллить обратно к нужному элементу
    await human_scroll(page, -random.randint(200, 400))
    await asyncio.sleep(random.uniform(0.5, 1.5))

    # Кликнуть
    await human_click(page, target_selector)

Что ещё проверяют антибот-системы:

  • navigator.webdriver — Playwright выставляет true по умолчанию, stealth-патч убирает это
  • Отсутствие движения мыши до клика — если первый же event это click, это подозрительно
  • Слишком быстрый DOMContentLoaded → клик — реальный пользователь не кликает через 50 мс после загрузки страницы
  • Отсутствие событий mouseover/mouseenter перед кликом — браузер генерирует их автоматически при реальном движении мыши, но не при программном click()

Необходимый минимум на практике:

Stealth закрывает только JS-маркеры автоматизации (navigator.webdriver, fingerprint и т.д.) — поведение мыши и тайминги он не трогает. Если сайт проверяет поведенческие события, нужно самому добавить хотя бы базовую имитацию.

Полная кривая Безье нужна редко — только против агрессивных систем (Akamai, Cloudflare Advanced Bot Management). Для большинства сайтов достаточно трёх вещей:

  1. Пауза после загрузки страницы — не кликать сразу после DOMContentLoaded
  2. Движение мыши к элементу перед кликом — чтобы браузер сгенерировал mouseover/mouseenter
  3. Случайное смещение точки клика внутри элемента — не точно в центр
python
import asyncio
import random

async def safe_click(page, selector: str):
    """Минимальная имитация: пауза + движение + смещение клика."""
    await asyncio.sleep(random.uniform(0.5, 1.5))  # пауза перед действием

    box = await page.locator(selector).bounding_box()
    x = box["x"] + box["width"] * random.uniform(0.3, 0.7)
    y = box["y"] + box["height"] * random.uniform(0.3, 0.7)

    await page.mouse.move(x, y)  # mouseover/mouseenter генерируются автоматически
    await asyncio.sleep(random.uniform(0.1, 0.3))
    await page.mouse.click(x, y)

Для ввода текста — встроенный delay в Playwright достаточен для большинства сайтов:

python
await page.locator(selector).type(text, delay=random.randint(50, 150))

Полные human_move, human_scroll и browse_naturally применяются, когда сайт конкретно детектит именно поведение мыши — это видно по тому, что бан происходит при клике или скролле, а не при загрузке страницы.











Прокси

Прокси — это посредник между нашим скрапером и целевым сайтом. Сайт видит IP прокси, а не наш. Это решает проблему блокировки по IP и позволяет распределять нагрузку между множеством адресов.


Типы прокси

DatacenterResidentialISP (Static Residential)Mobile (4G/5G)
ДетектируемостьВысокаяНизкаяНизкаяОчень низкая
СтабильностьВысокаяСредняяВысокаяСредняя
ЦенаДёшевоДорожеСредняяДорого
Когда использоватьСайты с низкой защитойБольшинство задачСкорость + скрытностьСамые защищённые сайты

Datacenter — IP от дата-центров (AWS, Azure, DigitalOcean). Быстрые и дешёвые, но у антибот-систем есть базы диапазонов ДЦ — их легко детектировать. Подходят для сайтов без серьёзной защиты или для предварительного тестирования.

Residential — IP реальных домашних пользователей, выданных интернет-провайдером. Трудно отличить от реального пользователя. Минус — иногда нестабильны: пользователь отключился — IP исчез.

ISP (Static Residential) — IP выдан провайдером, но физически размещён в ДЦ. Быстрые как datacenter, выглядят как residential. Хороший компромисс по цене и скрытности.

Mobile (4G/5G) — IP мобильных операторов. Один IP одновременно используют тысячи реальных пользователей через NAT, поэтому сайтам крайне сложно их блокировать — это затронет реальных людей. Самый дорогой тип.


Ротация прокси

Ротация — автоматическая смена IP при каждом запросе или через N запросов. Без ротации один IP быстро накапливает подозрительную активность и уходит в бан.


Rotating vs Sticky сессии

  • Rotating — новый IP на каждый запрос. Подходит для независимых GET-запросов (скрапинг списков, карточек товаров).
  • Sticky — один IP держится N минут. Нужен, когда запросы связаны между собой: логин → получение данных → следующая страница. Если IP меняется посреди сессии, сайт может её сбросить.

У платных провайдеров ротация работает через единый gateway-endpoint — провайдер сам меняет IP внутри своего пула. Не нужно хранить список адресов самостоятельно.

Ротация вручную через список прокси:

python
import random
import requests

PROXIES = [
    "http://user:pass@proxy1.example.com:8080",
    "http://user:pass@proxy2.example.com:8080",
    "http://user:pass@proxy3.example.com:8080",
]

def get_proxy() -> dict:
    proxy = random.choice(PROXIES)
    return {"http": proxy, "https": proxy}

for url in urls:
    response = requests.get(url, proxies=get_proxy(), timeout=30)

Через rotating endpoint провайдера (один адрес, IP меняется автоматически):

python
import requests

# BrightData, Smartproxy и аналоги дают один gateway — IP ротируется внутри
PROXY = "http://username:password@gate.smartproxy.com:10000"

session = requests.Session()
session.proxies = {"http": PROXY, "https": PROXY}

response = session.get("https://example.com/products")

Прокси в curl_cffi:

python
from curl_cffi import requests

response = requests.get(
    "https://example.com",
    impersonate="chrome124",
    proxies={"http": "http://user:pass@proxy.example.com:8080",
             "https": "http://user:pass@proxy.example.com:8080"},
)

Прокси в Playwright:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={
            "server": "http://proxy.example.com:8080",
            "username": "user",
            "password": "pass",
        }
    )
    page = browser.new_page()
    page.goto("https://example.com")

Охлаждение прокси

Охлаждение (cooldown) — период, в течение которого IP нельзя использовать после получения бана или подозрения. За это время временная блокировка на стороне сайта может сняться.

Почему это нужно: сайты часто банят IP не навсегда, а временно — на несколько минут или часов. Если продолжать слать запросы с того же IP, каждый из них уйдёт в бан мгновенно. Если дать IP "остыть" — он снова становится рабочим.

Пул прокси с cooldown-логикой:

python
import time
import random
import requests
from dataclasses import dataclass, field

@dataclass
class ProxyEntry:
    url: str
    banned_until: float = 0.0  # unix timestamp, до которого IP в карантине

class ProxyPool:
    def __init__(self, proxies: list[str], cooldown_seconds: int = 300):
        self.pool = [ProxyEntry(url=p) for p in proxies]
        self.cooldown = cooldown_seconds

    def get(self) -> ProxyEntry | None:
        """Возвращает случайный доступный прокси или None, если все в карантине."""
        now = time.monotonic()
        available = [p for p in self.pool if p.banned_until <= now]
        if not available:
            return None
        return random.choice(available)

    def ban(self, proxy: ProxyEntry):
        """Отправляет прокси в карантин на cooldown секунд."""
        proxy.banned_until = time.monotonic() + self.cooldown
        print(f"Proxy {proxy.url} banned for {self.cooldown}s")

    def request(self, url: str, **kwargs) -> requests.Response:
        """Делает запрос, автоматически меняя прокси при 403/429."""
        for attempt in range(len(self.pool)):
            proxy = self.get()
            if proxy is None:
                wait = min(p.banned_until for p in self.pool) - time.monotonic()
                print(f"All proxies in cooldown, waiting {wait:.0f}s...")
                time.sleep(max(wait, 0))
                proxy = self.get()

            proxies = {"http": proxy.url, "https": proxy.url}
            try:
                response = requests.get(url, proxies=proxies, timeout=30, **kwargs)
                if response.status_code in (403, 429):
                    self.ban(proxy)
                    continue
                return response
            except requests.RequestException:
                self.ban(proxy)
                continue

        raise RuntimeError("All proxies exhausted")


# Использование
pool = ProxyPool(
    proxies=[
        "http://user:pass@proxy1.example.com:8080",
        "http://user:pass@proxy2.example.com:8080",
        "http://user:pass@proxy3.example.com:8080",
    ],
    cooldown_seconds=300,  # 5 минут карантина после бана
)

for url in urls:
    response = pool.request(url)
    process(response.text)

Подводные камни

  • Уже забаненные IP — дешёвые прокси часто уже числятся в чёрных списках. Перед покупкой проверяем репутацию через scamalytics.com или ipinfo.io. Если у IP fraud score > 50 — скорее всего, уже заблокирован на большинстве сайтов.

  • Геолокация должна совпадать — прокси из Китая для US-сайта сразу выглядит подозрительно. Прокси, язык (Accept-Language), таймзона и User-Agent должны быть согласованы. Для LinkedIn, например, нужен прокси из того же города, что и аккаунт.

  • HTTP vs HTTPS vs SOCKS5 — через HTTP-прокси провайдер видит весь трафик, включая заголовки авторизации. Используем HTTPS (CONNECT tunnel) или SOCKS5, где это возможно.

  • Медленный прокси = таймаут — residential и mobile прокси медленнее datacenter. Ставим timeout не меньше 30 секунд, иначе получим ложные ошибки.

  • Прокси ≠ решение всех проблем — ротация IP помогает против блокировки по адресу, но не против fingerprint-детектирования, капч и поведенческого анализа. Прокси работают в связке с правильными заголовками, TLS-имитацией и jitter'ом.











Как искать данные без скрапинга?

Перед тем как начинать скрапинг — проверяем, нет ли готового решения (чтобы сэкономить время себе и деньги клиенту)

Порядок проверки:

  1. Есть ли официальный API?
  2. Есть ли готовый датасет или агрегатор данных?
  3. Есть ли готовый скрапер/инструмент, который уже умеет это собирать?
  4. Можно ли купить данные за адекватную цену?

Только если всё вышеперечисленное не подходит — начинаем писать скрапер с нуля.


Официальные и публичные API

Многие платформы предоставляют API. Даже если платное — часто дешевле, чем делать и поддерживать скрапер.

Как проверить:

  1. Идём на сайт → ищем раздел "Developers", "API", "Data" в футере или документации. Либо гуглим {название сайта} api documentation.

  2. Спрашиваем у ChatGPT или Claude, они часто предлагают аналоги или, если API спрятано, подсвечивают его.

Coingecko API

Готовые датасеты

Когда нужны исторические или статичные данные — часто их уже кто-то собрал.

Бесплатные источники:

  • Kaggle Datasets — огромная библиотека датасетов. Есть данные по e-commerce, недвижимости, финансам, NLP, гео и т.д. Поиск по ключевым словам.
  • Hugging Face Datasets — в основном для ML/NLP, но есть и бизнесовые датасеты.
  • Google Dataset Search — поисковик по публичным датасетам. Полезно для нишевых тем.
  • Common Crawl — открытый архив веба. Петабайты HTML, доступны как S3 bucket. Используют для обучения LLM, но можно извлечь и конкретные данные.

Как искать: {тема} dataset site:kaggle.com или просто {тема} open dataset csv. Или, опять же, спрашиваем AI.


Готовые скраперы и инструменты

Ищем на GitHub, есть ли готовые скраперы для нужной нам платформы.

Спрашиваем AI об этом, если не нашли сами.


Покупка данных у поставщиков

Когда нужны актуальные данные в больших объёмах — иногда дешевле купить, чем собрать.

Особенно много данных продают в сфере крипты и финансов.











Как извлекать данные

Структурированные данные (JSON, XML, CSV)

Самый простой случай — данные уже отдаются в структурированном виде. Обычно так происходит при работе с API.

python
import requests
import xmltodict
import csv

# JSON
data = requests.get("https://api.example.com/products").json()
print(data["items"][0]["price"])

# XML
response = requests.get("https://api.example.com/feed.xml")
data = xmltodict.parse(response.text)
print(data["feed"]["item"][0]["price"])

# CSV
import io
response = requests.get("https://example.com/export.csv")
reader = csv.DictReader(io.StringIO(response.text))
for row in reader:
    print(row["price"])

Никакого парсинга — просто читаем готовые данные.


Парсинг HTML через BeautifulSoup

Когда данных в API нет и приходится работать с HTML — используем BeautifulSoup4 + CSS-селекторы.

python
from bs4 import BeautifulSoup
import requests

html = requests.get("https://example.com/product/1").text
soup = BeautifulSoup(html, "html.parser")

# Одно совпадение
price = soup.select_one(".product-price").text.strip()

# Несколько совпадений
items = [el.text.strip() for el in soup.select(".product-card .title")]

Через LLM, если структура часто меняется

Когда верстка нестабильна, нет чётких селекторов или структура данных непредсказуема — скармливаем HTML или текст в LLM и просим вернуть нужные поля.

Используем дешевые mini-модели: gpt-5-nano, claude-haiku-4-5, gemini-2.0-flash (быстрые, дешевые).

python
from openai import OpenAI

client = OpenAI()

html_fragment = """
<div class="lot-info">
  <span>Lotto 6/45</span>
  <span class="draw-result">3 • 7 • 19 • 22 • 38 • 41</span>
  <span>Jackpot: $2,400,000</span>
</div>
"""

response = client.chat.completions.create(
    model="gpt-5-nano",
    messages=[
        {
            "role": "user",
            "content": f"Extract: numbers (list of ints), jackpot (string). Return JSON only.\n\n{html_fragment}"
        }
    ],
    response_format={"type": "json_object"},
)

import json
data = json.loads(response.choices[0].message.content)
# {"numbers": [3, 7, 19, 22, 38, 41], "jackpot": "$2,400,000"}

TIP

LLM-парсинг хорошо работает в паре с разделением на скрапинг и парсинг — сохраняем исходный HTML, а парсим отдельно. Если модель ошиблась, можно перепарсить без повторного скрапинга.


OCR, если текст на изображениях

Когда данные отрисованы в картинку или canvas и не попадают в DOM — распознаём текст через OCR (optical character recognition).

Так любят делать казино и букмекеры: коэффициенты, счета, лимиты ставок намеренно рендерятся в <canvas> или <img>, чтобы их нельзя было достать из DOM напрямую.

Бесплатно — pytesseract:

python
import pytesseract
from PIL import Image
import requests
from io import BytesIO

response = requests.get("https://example.com/odds-image.png")
img = Image.open(BytesIO(response.content))
text = pytesseract.image_to_string(img)
print(text)

Точнее — облачные сервисы (Google Vision, AWS Textract): лучше справляются со сложными шрифтами, поворотами, плохим контрастом. Платно, но дёшево при небольших объёмах.

Когда использовать: canvas-элементы, сканы, скриншоты мобильного приложения, приложения без доступного HTML.


Экранная читалка Android (Accessibility API)

Когда API не нашли, а данные есть только в мобильном приложении — читаем UI-дерево через Android Accessibility Service.

Работает без root. Android предоставляет дерево всех видимых UI-элементов с текстом, координатами и типами — можно получать данные, не трогая сетевой трафик.

Подробнее — в секции Скрапинг через Android.


Экранная читалка Windows (Win32 API)

Для десктопных приложений — читаем UI-элементы через Windows UI Automation API.

pywinauto позволяет находить окна, кнопки, таблицы и текстовые поля по имени или типу элемента и читать их содержимое без скриншотов.

python
from pywinauto import Application

app = Application(backend="uia").connect(title="TradingApp")
window = app.window(title="TradingApp")

price = window.child_window(auto_id="lblPrice", control_type="Text").window_text()
print(price)

Подробнее — в секции Скрапинг через десктопное приложение.











Скрапинг

Скрапинг через статичный HTML

Самый простой метод — делаем обычный GET запрос и парсим HTML. Работает, когда страница рендерится на сервере (SSR rendering), а не в браузере.

Прямой запрос:

python
import requests
from bs4 import BeautifulSoup

response = requests.get("https://example.com/product/1")

# Далее парсим HTML
soup = BeautifulSoup(response.text, "html.parser")
price = soup.select_one(".product-price").text

Через посредников (scraperapi.com и аналоги): они проксируют запросы, ротируют IP и сами решают базовые защиты. Удобно, чтобы не заморачиваться с собственной прокси-ротацией.


Скрапинг через API сайта

Наиболее предпочтительный метод. Если нашли API — половина работы уже сделана: данные структурированы, нет проблем с парсингом HTML, проще масштабировать.


Ищем API через браузер (DevTools, F12)

Самый универсальный способ. Пошагово:

  1. Открываем DevTools (F12) → вкладка Network
  2. Выставляем фильтр Fetch/XHR — это отсекает картинки, шрифты и CSS
  3. Нажимаем Preserve log (иначе запросы сбросятся при навигации)
  4. Загружаем страницу с нужными данными
  5. Ищем запросы, в ответе которых есть JSON с нужными данными — кликаем → вкладка Preview для удобного просмотра структуры
  6. Вкладка Headers — смотрим URL, метод, параметры, заголовки авторизации
  7. Вкладка Payload — тело запроса для POST

Часто сайт с "красивым" фронтом на самом деле дёргает чистый REST API — и этот же API можно дёргать напрямую.

TIP

Проверяйте мобильную версию сайта (m.example.com или через DevTools → иконка мобильного устройства). Мобильные версии часто используют более простые и менее защищённые API, так как оптимизированы под слабые устройства.


Сниффинг запросов через Android

Смотри секцию про Android. Там более развёрнуто про то, как в целом сниффить запросы через Android. В контексте этого раздела нас интересует "увидеть нужный запрос" и "как повторить его через API".


Как делать запросы

Через Copy as cURL → Python

Самый быстрый способ воспроизвести точный запрос с браузера — не писать его вручную, а скопировать из DevTools:

  1. В DevTools → Network находим нужный запрос → правая кнопка → Copy → Copy as cURL (bash)
  2. Идём на curlconverter.com и вставляем — получаем готовый Python-код с правильными заголовками, куками и телом запроса

Это особенно удобно, когда запрос содержит много заголовков или сложную авторизацию.


Curlconverter

Базовый запрос через requests:

python
import requests

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Accept": "application/json",
    "Accept-Language": "en-US,en;q=0.9",
    "Referer": "https://example.com/products",
})

response = session.get(
    "https://api.example.com/products",
    params={"page": 1, "per_page": 50},
)
response.raise_for_status()  # бросает исключение при 4xx/5xx
data = response.json()

Пагинация:

Большинство API используют один из трёх подходов:

python
# 1. Offset/page — классика
all_items = []
page = 1
while True:
    resp = session.get(url, params={"page": page, "per_page": 100})
    data = resp.json()
    items = data["items"]          # или data["results"], data["data"] — зависит от API
    if not items:
        break
    all_items.extend(items)
    if len(items) < 100:           # получили меньше, чем запросили — последняя страница
        break
    page += 1

# 2. Курсорная пагинация (cursor/next_token)
cursor = None
while True:
    params = {"limit": 100}
    if cursor:
        params["cursor"] = cursor
    data = session.get(url, params=params).json()
    all_items.extend(data["items"])
    cursor = data.get("next_cursor")
    if not cursor:
        break

# 3. next_url — API сам даёт ссылку на следующую страницу
next_url = "https://api.example.com/products"
while next_url:
    data = session.get(next_url).json()
    all_items.extend(data["results"])
    next_url = data.get("next")    # None — конец

Retry-логика и обработка ошибок:

Сети ненадёжны, API падают. Минимальный retry:

python
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def make_session() -> requests.Session:
    session = requests.Session()
    retry = Retry(
        total=5,
        backoff_factor=1,          # задержки: 1s, 2s, 4s, 8s, 16s
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "POST"],
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session

Для 429 (rate limit) часто нужно читать заголовок Retry-After:

python
response = session.get(url)
if response.status_code == 429:
    wait = int(response.headers.get("Retry-After", 60))
    time.sleep(wait)

Авторизация:

python
# Bearer token
session.headers["Authorization"] = f"Bearer {token}"

# Basic auth
session.auth = ("username", "password")

# API key в заголовке (название зависит от API)
session.headers["X-API-Key"] = api_key

# Refresh token при истечении
def get_token(session: requests.Session) -> str:
    resp = session.post(
        "https://api.example.com/auth/token",
        json={"username": USER, "password": PASS},
    )
    return resp.json()["access_token"]

Подмена TLS fingerprint:

Если стандартный requests блокируется — используйте curl-cffi с параметром impersonate. Подробнее с примерами кода: Обход защит → curl-cffi.


Скрапинг через рендеринг сайта

Нужен, когда данные генерируются JavaScript'ом и не присутствуют в исходном HTML. Также используем, когда сайт проверяет поведение браузера и обычные HTTP запросы блокирует.

Playwright HeadlessРеальный ChromeMultilogin / GoLogin
МасштабируемостьСотни параллельноОдин браузерСотни параллельно
Обход защитСредний (со stealth)ВысокийВысокий
Множество аккаунтовНет (один fingerprint)Нет (один профиль)Да (уникальный fingerprint каждому)
Деплой на серверДаНужен GUI / XvfbДа
ПроксиЗадаётся в кодеЗадаётся в кодеПривязан к профилю в UI/API
ЦенаБесплатноБесплатно$50–150/мес

Скрапинг через headless Playwright со Stealth плагином

Headless браузер — полноценный Chromium без графического интерфейса. Рендерит страницы, выполняет JavaScript, управляет куками — как обычный браузер, но без окна. Управляется программно через API.

Главный плюс — легко масштабируется: можно запускать сотни браузеров параллельно, вопрос только в ресурсах сервера.

Стандартный Playwright легко детектируется: у него видны признаки автоматизации — специфичные JS-переменные (navigator.webdriver), нестандартные заголовки, отсутствие плагинов. playwright-stealth патчит эти точки детектирования.

Установка:

bash
pip install playwright playwright-stealth
playwright install chromium

Пример:

python
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=True,
        proxy={
            "server": "http://proxy.example.com:8080",
            "username": "user",
            "password": "pass",
        },
    )
    page = browser.new_page()
    stealth_sync(page)  # применяем stealth-патчи

    page.goto("https://example.com/products")
    page.wait_for_selector(".product-list")

    items = page.locator(".product-item").all()
    for item in items:
        print(item.locator(".title").text_content())

    browser.close()

Скрапинг через реальный Chrome

Запускаем обычный установленный Chrome с открытым CDP-портом и подключаемся к нему через Playwright. Браузер настоящий — с реальным профилем, расширениями и куками. Антибот-системам значительно сложнее его обнаружить.

Запуск Chrome:

powershell
& "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="C:\ChromeProfile"

Подключение через Playwright:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://localhost:9222")
    page = browser.contexts[0].pages[0]

    page.goto("https://example.com/products")
    items = page.locator(".product-item").all()
    for item in items:
        print(item.locator(".title").text_content())

Когда использовать:

  • Сайт требует авторизации — залогинился один раз, куки сохранились в профиле
  • Playwright Stealth не справляется с защитой

Ограничения: не масштабируется (один Chrome на одну машину), требует GUI, не подходит для серверного деплоя без виртуального дисплея.


Скрапинг через платный браузер (Multilogin / GoLogin)

Антидетект-браузеры — Multilogin, GoLogin, NST Browser — это модифицированный Chromium с подменой fingerprint'а на уровне движка: Canvas, WebGL, AudioContext, шрифты, разрешение, часовой пояс, User-Agent. Каждый профиль — уникальный "виртуальный компьютер".

Когда использовать:

  • Нужна работа с множеством аккаунтов
  • Сайт использует продвинутые fingerprint-системы (Kasada, Datadome, Akamai)

Главное отличие от реального Chrome — возможность запускать сотни изолированных профилей параллельно, каждый с уникальным fingerprint'ом.

Управление программно (Multilogin + Playwright):

Multilogin запускает профиль через локальный агент и отдаёт WebSocket-адрес — подключаемся через CDP.

Прокси привязывается к профилю заранее — через UI Multilogin или через API при создании/обновлении профиля. В коде ничего дополнительно указывать не нужно: профиль уже "знает" свой прокси.

Привязать прокси к профилю через API:

python
import requests

ML_TOKEN   = "your_multilogin_token"
PROFILE_ID = "your_profile_id"

requests.patch(
    f"http://localhost:35000/api/v1/profile/{PROFILE_ID}",
    headers={"Authorization": f"Bearer {ML_TOKEN}"},
    json={
        "proxy": {
            "type": "HTTP",
            "host": "proxy.example.com",
            "port": 8080,
            "username": "user",
            "password": "pass",
        }
    },
)

Запуск профиля и скрапинг:

python
import requests
from playwright.sync_api import sync_playwright

ML_TOKEN    = "your_multilogin_token"
PROFILE_ID  = "your_profile_id"

# Запускаем профиль через локальный агент Multilogin (порт 35000)
resp = requests.get(
    f"http://localhost:35000/api/v1/profile/start?automation=true&profileId={PROFILE_ID}",
    headers={"Authorization": f"Bearer {ML_TOKEN}"},
).json()
ws_url = resp["value"]  # wss://...

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(ws_url)
    page = browser.contexts[0].pages[0]

    page.goto("https://example.com")
    print(page.title())

    browser.close()

# Останавливаем профиль
requests.get(
    f"http://localhost:35000/api/v1/profile/stop?profileId={PROFILE_ID}",
    headers={"Authorization": f"Bearer {ML_TOKEN}"},
)

DevTools Protocol (CDP)

Playwright использует Chrome DevTools Protocol (CDP) под капотом. Иногда удобно работать с ним напрямую — перехватывать сетевые запросы прямо в скрапере.

Перехват сетевых ответов (извлекаем JSON из XHR-запросов):

python
from playwright.sync_api import sync_playwright

results = []

def handle_response(response):
    if "api/products" in response.url:
        try:
            results.extend(response.json().get("items", []))
        except Exception:
            pass

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.on("response", handle_response)  # подписываемся на все ответы

    page.goto("https://example.com/products")
    page.wait_for_load_state("networkidle")

    browser.close()

print(results)

Как решать капчу

Сервисы-решатели (самый универсальный способ):

2captcha, AntiCaptcha — используют людей или AI. Поддерживают reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile.

python
import time
import requests

API_KEY = "your_2captcha_key"

def solve_recaptcha(site_key: str, page_url: str) -> str:
    resp = requests.post("https://2captcha.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1,
    })
    task_id = resp.json()["request"]

    for _ in range(20):
        time.sleep(5)
        result = requests.get("https://2captcha.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": 1,
        }).json()
        if result["status"] == 1:
            return result["request"]

    raise TimeoutError("Captcha not solved in time")

Подстановка токена через Playwright:

python
token = solve_recaptcha("6Le-wvkSAAAAA...", page.url)

# Для reCAPTCHA v2 — вставляем токен в скрытое поле и сабмитим
page.evaluate(f"document.getElementById('g-recaptcha-response').innerHTML = '{token}'")
page.click("#submit-btn")

Через playwright-recaptcha (автоматически, без внешнего сервиса):

bash
pip install playwright-recaptcha
python
from playwright.sync_api import sync_playwright
from playwright_recaptcha import recaptchav2

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/login")

    with recaptchav2.SyncSolver(page) as solver:
        token = solver.solve_recaptcha(wait=True)

    page.click("#submit")
    browser.close()

Когда что использовать:

СитуацияИнструмент
Обычный JS-рендеринг + базовая антибот-защитаPlaywright Stealth
Серьёзная защита, один аккаунтРеальный Chrome через CDP
Множество аккаунтов или продвинутые fingerprint-проверкиMultilogin / GoLogin / NST

Скрапинг через браузерное расширение

Используется, когда сайт имеет особо строгую защиту, которая пропускает только "настоящие" расширения, или когда нужен доступ к данным страницы изнутри браузерного контекста.

Расширение работает в контексте браузера и имеет доступ к DOM, куки, localStorage — без ограничений CORS и без детектирования, характерного для автоматизации.


Web Scraper

Web Scraper — бесплатное Chrome/Firefox-расширение для разового или нерегулярного сбора данных без написания кода. Подходит для одноразовых задач на несколько тысяч страниц, когда писать полноценный скрапер нецелесообразно.

Как работает:

  1. Устанавливаем расширение из Chrome Web Store
  2. Открываем DevTools → вкладка Web Scraper
  3. Создаём sitemap — описываем структуру сайта: стартовый URL, селекторы элементов, пагинацию
  4. Запускаем скрапинг — расширение само обходит страницы и собирает данные
  5. Экспортируем результат в CSV или JSON

Когда использовать:

  • Разово снять данные с сайта без сложной защиты
  • Нет смысла писать скрипт

Ограничения:

  • Работает только локально в браузере — не масштабируется и не автоматизируется
  • Не справится с сайтами, где нужна авторизация или сложная логика
  • Не подходит для регулярного скрапинга

Скрапинг Windows приложения

Используется, когда данные доступны только через десктопное приложение — нет веб-версии, нет мобильного API, нет публичного эндпоинта. Типичные примеры: биржевые терминалы, корпоративные ERP, специализированный B2B-софт.

Алгоритм: сначала пробуем перехватить сетевой трафик — это самый надёжный способ. Если приложение не делает HTTP-запросов или трафик не поддаётся перехвату — читаем UI-дерево через pywinauto. Если контролы нестандартные — пробуем pyautogui.


Шаг 1: Сниффинг запросов

Многие десктопные приложения общаются с сервером по HTTP/HTTPS. Если перехватить эти запросы — можно обращаться к тому же API напрямую, минуя само приложение.

Инструменты:

  • mitmproxy — open-source прокси, удобен для Python-скриптов
  • Fiddler — GUI-инструмент, проще для начального исследования

Настройка mitmproxy для HTTPS:

  1. Устанавливаем mitmproxy: pip install mitmproxy
  2. Запускаем: mitmproxy --mode regular --listen-port 8080
  3. Устанавливаем корневой сертификат mitmproxy в Windows (автоматически после первого запуска лежит в %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer) — добавляем в "Доверенные корневые центры сертификации"
  4. Выставляем системный прокси: 127.0.0.1:8080
  5. Запускаем целевое приложение и смотрим трафик

Автоматический перехват через inline-скрипт:

python
# intercept.py — запуск: mitmproxy -s intercept.py
from mitmproxy import http
import json

def response(flow: http.HTTPFlow):
    if "api.example.com" in flow.request.pretty_url:
        data = json.loads(flow.response.content)
        print(data)

Если нашли нужный эндпоинт — дальше работаем с ним напрямую через requests, без запуска приложения.


Шаг 2: UI Automation через pywinauto

Если перехват трафика не подходит — читаем UI-дерево Windows напрямую. Работает со стандартными Win32, WPF и UIA-приложениями.

pip install pywinauto

Подключение и чтение элемента:

python
from pywinauto import Application

app = Application(backend="uia").connect(title_re=".*TradingApp.*")
win = app.top_window()

price = win.child_window(auto_id="lblPrice", control_type="Text").window_text()
print(price)

Итерация по строкам таблицы:

python
from pywinauto import Application

app = Application(backend="uia").connect(title_re=".*TradingApp.*")
win = app.top_window()

table = win.child_window(control_type="DataGrid")
rows = table.children(control_type="DataItem")

for row in rows:
    cells = row.children(control_type="Custom")
    print([c.window_text() for c in cells])

Инспекция дерева элементов — чтобы найти нужные auto_id и control_type:

python
win.print_control_identifiers()

Шаг 3: Автоматизация через pyautogui

Когда pywinauto не видит контролы — приложение рисует UI самостоятельно (Electron, Qt, кастомный рендер). Автоматизируем мышь и клавиатуру, данные читаем через OCR.

pip install pyautogui pillow pytesseract

Кликнуть кнопку и прочитать область экрана:

python
import pyautogui
import pytesseract
from PIL import ImageGrab

pyautogui.click(x=320, y=150)
pyautogui.sleep(1)

# Снимаем нужную область (left, top, right, bottom)
screenshot = ImageGrab.grab(bbox=(100, 200, 800, 600))
text = pytesseract.image_to_string(screenshot, lang="rus+eng")
print(text)

Найти кнопку по картинке (устойчивее к изменению позиции окна):

python
import pyautogui

btn_pos = pyautogui.locateOnScreen("refresh_btn.png", confidence=0.9)
if btn_pos:
    pyautogui.click(btn_pos)

Tesseract нужно установить отдельно: github.com/UB-Mannheim/tesseract. Путь прописываем в pytesseract.pytesseract.tesseract_cmd.


Когда что использовать

СитуацияИнструмент
Приложение делает HTTP/HTTPS запросы к серверуmitmproxy / Fiddler
Стандартные Win32 / WPF / UIA-контролыpywinauto
Кастомный рендеринг, Electron, Qtpyautogui + OCR