Что такое лимиты скорости при скрапинге?
Лимиты скорости (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 браузер | Сложно (используйте браузер) |
| Исполнение JavaScript | Headless-браузер | Сложно (настройка) |
| События мыши/клавиатуры | Поведение бота | Очень сложно |
Подробнее о механизмах обнаружения — в руководстве Как антибот-системы обнаруживают прокси.
HTTP-коды, сигнализирующие об ограничении
Знание HTTP-кодов, означающих ограничение скорости, помогает правильно строить логику повторов:
| Код | Значение | Действие |
|---|---|---|
| 200 (с CAPTCHA) | Мягкая блокировка — страница-челлендж | Ротируйте IP, замедлитесь |
| 403 Forbidden | IP или сессия заблокированы | Немедленно ротируйте 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. Всегда начинайте консервативно и увеличивайте на основе наблюдаемого процента успеха.






