Лимиты скорости при скрапинге: полное объяснение

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

Лимиты скорости при скрапинге: полное объяснение

Что такое лимиты скорости при скрапинге?

Лимиты скорости (rate limits) — это невидимые стены, которые веб-сайты строят для контроля частоты запросов от любого клиента. Когда вы скрапите сайт слишком агрессивно, вы упираетесь в эти стены — и последствия варьируются от временных замедлений до постоянных банов IP. Понимание того, как работают лимиты, как вас обнаруживают и как оставаться в пределах допустимого — основа надёжного скрапинга.

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

Для общего обзора скрапинга с прокси смотрите наше Полное руководство по прокси для веб-скрапинга. Об избежании блокировок в целом — Как скрапить сайты без блокировки.

Как работает ограничение скорости

Веб-сайты реализуют лимиты на нескольких уровнях, каждый с разной степенью детализации:

Уровень 1: Лимиты по IP

Самый распространённый подход. Сервер отслеживает запросы с каждого IP в пределах временного окна. Превысили порог — получаете HTTP 429 (Too Many Requests) или 503.

# Typical rate limit behavior
Request 1-50:    HTTP 200 (normal)
Request 51:      HTTP 429 (rate limited)
Wait 60 seconds...
Request 52:      HTTP 200 (reset)

Уровень 2: Лимиты по сессии/cookie

Отслеживает частоту запросов по сессии или cookie браузера. Даже если вы ротируете IP, один и тот же токен сессии, попадающий на сервер с высокой частотой, вызовет ограничение.

Уровень 3: Лимиты по аккаунту

Для сайтов с авторизацией лимиты привязаны к учётной записи независимо от IP. Типично для API и SaaS-платформ.

Уровень 4: Поведенческий анализ

Продвинутые системы вроде Cloudflare, PerimeterX и Akamai анализируют поведенческие паттерны: тайминг запросов, навигационный поток, движения мыши (в контексте браузера). Этот уровень сложнее всего обойти, так как он не опирается на простые счётчики.

Распространённые сигналы обнаружения

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

СигналЧто обнаруживаетСложность обхода
Запросы с IP в минутуСкоростьЛегко (используйте прокси)
Запросы с IP в час/деньУстойчивый объёмСредне (ротируйте IP)
Регулярность таймингаМашиноподобные интервалыСредне (добавьте дрожание)
Отсутствие/неверные заголовкиНебраузерные клиентыЛегко (установите заголовки)
Последовательные паттерны URLСистематический обходСредне (рандомизируйте порядок)
TLS-отпечатокБиблиотека vs браузерСложно (используйте браузер)
Исполнение JavaScriptHeadless-браузерСложно (настройка)
События мыши/клавиатурыПоведение ботаОчень сложно

Подробнее о механизмах обнаружения — в руководстве Как антибот-системы обнаруживают прокси.

HTTP-коды, сигнализирующие об ограничении

Знание HTTP-кодов, означающих ограничение скорости, помогает правильно строить логику повторов:

КодЗначениеДействие
200 (с CAPTCHA)Мягкая блокировка — страница-челленджРотируйте IP, замедлитесь
403 ForbiddenIP или сессия заблокированыНемедленно ротируйте IP
429 Too Many RequestsЯвное срабатывание лимитаПодождите и повторите с задержкой
503 Service UnavailableПерегрузка сервера или блокировкаЗадержка, проверьте блокировку
302/307 на URL CAPTCHAРедирект на челленджРотируйте IP, снизьте скорость

Стратегия 1: Уважительное регулирование

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

import requests
import time
import random
PROXY = "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
def respectful_scrape(urls: list[str], rpm_limit: int = 10) -> list[str]:
    """Scrape URLs while respecting a requests-per-minute limit."""
    delay = 60.0 / rpm_limit
    results = []
    for url in urls:
        try:
            resp = requests.get(
                url,
                proxies={"http": PROXY, "https": PROXY},
                timeout=30
            )
            results.append(resp.text if resp.status_code == 200 else None)
        except requests.RequestException:
            results.append(None)
        # Add delay with random jitter (±30%) to look less robotic
        jitter = delay * random.uniform(0.7, 1.3)
        time.sleep(jitter)
    return results

Стратегия 2: Адаптивное регулирование

Вместо фиксированной скорости динамически корректируйте её на основе получаемых ответов. Ускоряйтесь, когда всё работает, замедляйтесь при появлении предупреждений.

Реализация на Python

import requests
import time
import random
from dataclasses import dataclass, field
PROXY = "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
@dataclass
class AdaptiveThrottle:
    """Automatically adjusts request rate based on server responses."""
    base_delay: float = 2.0      # seconds between requests
    min_delay: float = 0.5
    max_delay: float = 30.0
    current_delay: float = 2.0
    success_streak: int = 0
    warning_codes: set = field(default_factory=lambda: {429, 403, 503})
    def on_success(self):
        self.success_streak += 1
        # Speed up after 10 consecutive successes
        if self.success_streak >= 10:
            self.current_delay = max(self.current_delay * 0.85, self.min_delay)
            self.success_streak = 0
    def on_rate_limit(self):
        self.success_streak = 0
        # Double the delay on rate limit
        self.current_delay = min(self.current_delay * 2.0, self.max_delay)
    def on_block(self):
        self.success_streak = 0
        # Aggressive backoff on block
        self.current_delay = min(self.current_delay * 3.0, self.max_delay)
    def wait(self):
        jitter = self.current_delay * random.uniform(0.7, 1.3)
        time.sleep(jitter)
def scrape_adaptive(urls: list[str]) -> list[dict]:
    throttle = AdaptiveThrottle()
    results = []
    for url in urls:
        try:
            resp = requests.get(
                url,
                proxies={"http": PROXY, "https": PROXY},
                timeout=30
            )
            if resp.status_code == 200:
                throttle.on_success()
                results.append({"url": url, "status": 200, "body": resp.text})
            elif resp.status_code == 429:
                throttle.on_rate_limit()
                # Check Retry-After header
                retry_after = int(resp.headers.get("Retry-After", 0))
                if retry_after:
                    time.sleep(retry_after)
                results.append({"url": url, "status": 429, "body": None})
            elif resp.status_code == 403:
                throttle.on_block()
                results.append({"url": url, "status": 403, "body": None})
            else:
                results.append({"url": url, "status": resp.status_code, "body": resp.text})
        except requests.RequestException as e:
            throttle.on_block()
            results.append({"url": url, "status": 0, "error": str(e)})
        throttle.wait()
        print(f"Current delay: {throttle.current_delay:.1f}s")
    return results

Реализация на Node.js

const HttpsProxyAgent = require('https-proxy-agent');
const fetch = require('node-fetch');
class AdaptiveThrottle {
  constructor() {
    this.currentDelay = 2000; // ms
    this.minDelay = 500;
    this.maxDelay = 30000;
    this.successStreak = 0;
  }
  onSuccess() {
    this.successStreak++;
    if (this.successStreak >= 10) {
      this.currentDelay = Math.max(this.currentDelay * 0.85, this.minDelay);
      this.successStreak = 0;
    }
  }
  onRateLimit() {
    this.successStreak = 0;
    this.currentDelay = Math.min(this.currentDelay * 2, this.maxDelay);
  }
  onBlock() {
    this.successStreak = 0;
    this.currentDelay = Math.min(this.currentDelay * 3, this.maxDelay);
  }
  async wait() {
    const jitter = this.currentDelay * (0.7 + Math.random() * 0.6);
    return new Promise(resolve => setTimeout(resolve, jitter));
  }
}
async function scrapeAdaptive(urls) {
  const throttle = new AdaptiveThrottle();
  const agent = new HttpsProxyAgent('http://USERNAME:PASSWORD@gate.proxyhat.com:8080');
  const results = [];
  for (const url of urls) {
    try {
      const res = await fetch(url, { agent, timeout: 30000 });
      if (res.ok) {
        throttle.onSuccess();
        results.push({ url, status: res.status, body: await res.text() });
      } else if (res.status === 429) {
        throttle.onRateLimit();
        const retryAfter = parseInt(res.headers.get('retry-after') || '0');
        if (retryAfter) await new Promise(r => setTimeout(r, retryAfter * 1000));
        results.push({ url, status: 429, body: null });
      } else if (res.status === 403) {
        throttle.onBlock();
        results.push({ url, status: 403, body: null });
      }
    } catch (err) {
      throttle.onBlock();
      results.push({ url, status: 0, error: err.message });
    }
    await throttle.wait();
    console.log(`Current delay: ${throttle.currentDelay.toFixed(0)}ms`);
  }
  return results;
}

Стратегия 3: Распределённое ограничение скорости

При запуске нескольких экземпляров скрапера параллельно координируйте ограничение между всеми воркерами. Без координации каждый воркер соблюдает свой лимит, но совокупный трафик всё равно перегружает цель.

import requests
import time
import threading
class DistributedRateLimiter:
    """Thread-safe rate limiter for multiple scraper workers."""
    def __init__(self, max_rpm: int):
        self.min_interval = 60.0 / max_rpm
        self.lock = threading.Lock()
        self.last_request_time = 0.0
    def acquire(self):
        """Block until it is safe to make the next request."""
        with self.lock:
            now = time.time()
            elapsed = now - self.last_request_time
            if elapsed < self.min_interval:
                time.sleep(self.min_interval - elapsed)
            self.last_request_time = time.time()
# Shared limiter across all threads
limiter = DistributedRateLimiter(max_rpm=30)
PROXY = "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
def worker(urls: list[str], results: list):
    for url in urls:
        limiter.acquire()
        try:
            resp = requests.get(
                url,
                proxies={"http": PROXY, "https": PROXY},
                timeout=30
            )
            results.append({"url": url, "status": resp.status_code})
        except Exception as e:
            results.append({"url": url, "error": str(e)})

Стратегия 4: Очередь запросов с приоритетом

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

import requests
import time
import heapq
import threading
from collections import defaultdict
PROXY = "http://USERNAME:PASSWORD@gate.proxyhat.com:8080"
class DomainRateLimiter:
    """Per-domain rate limiting with priority queue."""
    def __init__(self, default_rpm: int = 10):
        self.default_rpm = default_rpm
        self.domain_limits = {}          # domain -> max RPM
        self.domain_last = defaultdict(float)  # domain -> last request time
        self.lock = threading.Lock()
    def set_limit(self, domain: str, rpm: int):
        self.domain_limits[domain] = rpm
    def wait_for_domain(self, domain: str):
        rpm = self.domain_limits.get(domain, self.default_rpm)
        min_interval = 60.0 / rpm
        with self.lock:
            now = time.time()
            elapsed = now - self.domain_last[domain]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            self.domain_last[domain] = time.time()
# Configure per-domain limits
limiter = DomainRateLimiter(default_rpm=10)
limiter.set_limit("amazon.com", 3)        # Very conservative for Amazon
limiter.set_limit("example.com", 30)      # Lenient for simple sites
limiter.set_limit("google.com", 5)        # Moderate for Google

Чтение robots.txt для подсказок о лимитах

Многие сайты публикуют предпочтения обхода в robots.txt. Директива Crawl-delay указывает минимальное время между запросами:

import requests
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser
def get_crawl_delay(base_url: str, user_agent: str = "*") -> float | None:
    """Extract Crawl-delay from robots.txt."""
    parsed = urlparse(base_url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
    try:
        resp = requests.get(robots_url, timeout=10)
        if resp.status_code != 200:
            return None
        rp = RobotFileParser()
        rp.parse(resp.text.splitlines())
        delay = rp.crawl_delay(user_agent)
        return delay
    except Exception:
        return None
# Check before scraping
delay = get_crawl_delay("https://example.com")
if delay:
    print(f"Site requests {delay}s between requests")
else:
    print("No crawl-delay specified")

Частые ошибки с лимитами скорости

  • Игнорирование 429 ответов. Многие скраперы одинаково обрабатывают все не-200 ответы. Код 429 точно указывает проблему — используйте заголовок Retry-After и замедлитесь.
  • Фиксированные задержки без дрожания. Запрос ровно каждые 2.000 секунды выглядит роботизированно. Добавьте случайную вариацию к задержкам.
  • Отсутствие координации параллельных воркеров. Пять воркеров по 10 RPM каждый — это 50 RPM суммарно. Используйте общий ограничитель.
  • Ротация IP без замедления. Ротация IP даёт время, но если каждый новый IP тут же заваливает сайт запросами, продвинутая детекция всё равно вас поймает. Комбинируйте ротацию с правильным регулированием.
  • Скрапинг в пиковые часы. Сайты агрессивнее ограничивают скорость в период высокого трафика. Планируйте тяжёлые обходы на нерабочие часы в часовом поясе цели.

Чтобы рассчитать, сколько прокси нужно для вашего скрапинга с лимитами, смотрите Сколько прокси нужно для скрапинга?. О стратегиях ротации, дополняющих лимиты скорости, читайте Стратегии ротации прокси для масштабного скрапинга.

Начните скрапинг с правильными лимитами с помощью ProxyHat Python SDK или изучите тарифные планы для вашего проекта.

Часто задаваемые вопросы

Что происходит при превышении лимита скорости?

Ответ зависит от сайта. Большинство возвращают HTTP 429 с заголовком Retry-After. Некоторые показывают CAPTCHA. Агрессивные сайты немедленно блокируют IP ответом 403. В худшем случае повторные нарушения ведут к постоянным банам IP.

Как узнать лимит скорости сайта?

Начните медленно и увеличивайте постепенно, отслеживая коды ответов. Проверьте robots.txt на наличие директивы Crawl-delay. Наблюдайте за заголовками X-RateLimit-Limit и X-RateLimit-Remaining. Некоторые API публикуют лимиты в документации.

Прокси обходят лимиты скорости?

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

Какая самая безопасная частота запросов для скрапинга?

Универсального ответа нет. Для агрессивных целей вроде Google или Amazon безопасны 1-5 запросов в минуту на IP. Для слабо защищённых сайтов могут подойти 20-60 RPM на IP. Всегда начинайте консервативно и увеличивайте на основе наблюдаемого процента успеха.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог