Co to są Granice Stopniowe?
Oceń limity są niewidzialne ściany, które strony internetowe budować, aby kontrolować jak szybko każdy klient może złożyć wnioski. Gdy zbyt agresywnie drapiesz stronę, uderzasz w te ściany, a konsekwencje sięgają od tymczasowych spowolnień po stałe zakazy IP. Zrozumienie, jak działają ograniczenia stóp procentowych, jak cię wykrywają i jak pozostać pod nimi, ma zasadnicze znaczenie dla budowania skrobaków, które dostarczają wiarygodnych danych.
Ten przewodnik wyjaśnia mechanikę za ograniczeniem prędkości, sygnały detekcji stron internetowych używać, i praktyczne strategie adaptive throttling, które utrzymują Twoje scrapers działa sprawnie.
Aby uzyskać szerszy przegląd drapania z proxy, zobacz nasz Kompletny przewodnik do Web Scraping Proxies. Do unikania bloków w ogóle, czytaj Jak scalić strony internetowe bez blokowania.
Jak Rate Limiting Works
Strony internetowe wdrażają limity szybkości na wielu warstwach, z których każda ma inną ziarnistość wykrywania:
Warstwa 1: Limity stawki IP-
Najczęstsze podejście. Serwer śledzi żądania na adres IP w oknie czasowym. Przekrocz próg i otrzymasz odpowiedzi HTTP 429 (Zbyt wiele żądań) lub 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)Warstwa 2: Limity oparte na sesjach / ciasteczkach
Ślady żądają częstotliwości na sesję lub cookie przeglądarki. Nawet jeśli obrócisz IP, ten sam token sesji uderzając serwer szybko uruchomi ograniczenia.
Warstwa 3: Granice oparte na rachunkach
Dla stron wymagających logowania, limity są powiązane z kontem użytkownika niezależnie od IP. Wspólne dla platform API i SaaS.
Warstwa 4: Analiza behawioralna
Zaawansowane systemy, takie jak Cloudflare, PerimeterX i Akamai analizują wzorce behawioralne: czas oczekiwania, przepływ nawigacji, ruchy myszy (w kontekstach przeglądarki). Warstwa ta jest najtrudniejsza do obejścia, ponieważ nie polega na prostych licznikach.
Wspólne sygnały wykrywania wartości granicznych
Strony internetowe wykorzystują jednocześnie wiele sygnałów do wykrywania zautomatyzowanego skrobania:
| Sygnał | Co wykrywa | Trudności z unikaniem |
|---|---|---|
| Wnioski na IP na minutę | Prędkość surowca | Łatwe (używać proxy) |
| Wnioski dotyczące OD na godzinę / dzień | Trwała objętość | Średnia (obrót IP) |
| Żądanie regularności w czasie | Przedziały maszynowe | Średni (dodać jitter) |
| Brak / niewłaściwe nagłówki | Klienci niebędący przeglądarkami | Łatwe (ustawić właściwe nagłówki) |
| Sekwencyjne wzory URL | Systematyczne pełzanie | Średnia (kolejność losowa) |
| Odcisk palca TLS | Biblioteka vs przeglądarka | Twarda (użyj prawdziwych przeglądarek) |
| Wykonanie JavaScript | Przeglądarka bez głowy | Twarda (zaawansowana konfiguracja) |
| Wydarzenia myszy / klawiatury | Zachowanie bot | Bardzo trudne |
Dowiedz się więcej o mechanizmach wykrywania w naszym przewodniku Jak systemy Anti- Bot wykrywają efekty.
Kod odpowiedzi HTTP, który ogranicza szybkość sygnału
Wiedząc, które kody HTTP wskazują ograniczenie szybkości pomaga zbudować właściwą logikę ponownego testowania:
| Kod | Znaczenie | Działanie |
|---|---|---|
| 200 (z CAPTCHA) | Miękki blok - strona wyzwanie serwowane | Obróć IP, zwolnij |
| 403 Zakazane | IP lub sesja zablokowana | Obróć IP natychmiast |
| 429 Zbyt wiele żądań | Wyrażone ograniczenie prędkości | Poczekaj i spróbuj ponownie z cofnięciem |
| 503 Usługi niedostępne | Przeciążenie lub blok serwera | Backoff, sprawdź, czy zablokowane |
| 302 / 307 do adresu CAPTCHA | Wyzwanie przekierować | Obróć IP, zmniejsz prędkość |
Strategia 1: Prawidłowe przeciążenie
Najprostsze podejście - utrzymać swój wskaźnik zapotrzebowania znacznie poniżej tego, co pozwala cel. Oznacza to mniej awarii, mniej marnotrawstwa przepustowości i bardziej zrównoważone skrobanie.
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 resultsStrategia 2: Próg adaptacyjny
Zamiast stałej stawki, dynamicznie dostosowuj prędkość w oparciu o otrzymane odpowiedzi. Przyspiesz, gdy wszystko działa, zwolnij, gdy zobaczysz znaki ostrzegawcze.
Wdrażanie Pythona
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 resultsWdrażanie 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;
}Strategia 3: Ograniczenie rozłożone
Podczas równoległego uruchamiania wielokrotnych przypadków skrobania, należy koordynować limit szybkości we wszystkich pracownikach. Bez koordynacji każdy pracownik przestrzega własnego limitu, ale łączny ruch nadal przeważa nad celem.
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)})Strategia 4: Kolejka wniosków o priorytet
W przypadku złożonych projektów scrating należy stosować kolejkę priorytetową, która zarządza limitami stawek na domenę docelową:
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 GoogleCzytanie Robots.txt dla rate podpowiedzi
Wiele stron publikuje swoje preferencje pełzania w robot.txt. W Crawl-delay Dyrektywa mówi o minimalnych sekundach pomiędzy wnioskami:
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")Błędy w stawce wspólnej
- Ignoruję 429 odpowiedzi. Wiele scraperów traktuje wszystkie odpowiedzi nie-200 tak samo. 429 mówi dokładnie, co się stało - użyj nagłówka Retri- After i wycofać.
- Naprawiono opóźnienia bez jittera. Prośba dokładnie co 2.000 sekund wygląda robotycznie. Dodaj losową zmienność (jitter) do swoich opóźnień.
- Nie koordynuje równoległych pracowników. Pięciu robotników, każdy robi 10 RPM równa się 50 RPM razem. Użyj ogranicznika wspólnej stopy procentowej.
- Obrócanie IP bez zwalniania. Rotacja IP daje Ci czas, ale jeśli każdy nowy IP natychmiast młotkuje stronę, zaawansowana detekcja będzie nadal złapać. Połącz rotację z odpowiednią przepustnicą.
- Drapanie w godzinach szczytu. Miejsca są bardziej agresywne z ograniczeniem stawek w okresach wysokiego ruchu. Harmonogram ciężkich czołgów w godzinach poza szczytem dla strefy czasowej celu.
Aby obliczyć, ile proxy potrzebujesz do wsparcia swojego rate- ograniczone skrobanie, zobacz Ile nagród potrzebujesz do skrobania?Dla strategii rotacji proxy, które uzupełniają ograniczenie stóp, czytaj Strategie rotacji proxy dla rozdrabniania na dużą skalę.
Zacznij od odpowiednio ograniczonego drapania za pomocą ProxyHat Python SDK lub odkrywać plany cenowe dla twojego projektu.
Często zadawane pytania
Co się stanie, gdy przekroczę limit stawki?
Odpowiedź zależy od strony. Większość zwraca HTTP 429 z nagłówkiem Retri- After. Niektórzy podają CAPTCHA. Agresywne strony natychmiast blokują IP za pomocą odpowiedzi 403. W najgorszym wypadku powtarzające się naruszenia prowadzą do stałych zakazów IP.
Jak znaleźć limit stawki witryny?
Zacznij powoli i stopniowo wzrastać, monitorując kody odpowiedzi. Sprawdzić robotys.txt dla dyrektyw Opóźnienia Crawl. Obserwuj nagłówki odpowiedzi dla pól X- RateLimit - Limit i X- RateLimit - Pozostałe pola. Niektóre API publikują swoje ograniczenia w dokumentacji.
Czy użycie proxy jest ograniczone?
Proxies rozprowadzają wnioski w wielu IP, więc każdy IP pozostaje w granicach per- IP. Jednak zaawansowane strony również śledzić sesje, odciski palców i wzorce zachowań. Proxy są niezbędne, ale nie wystarczające - połączyć je z odpowiednim przepustnicy i realistyczne wzory żądania.
Jaka jest najbezpieczniejsza stawka za skrobanie?
Nie ma uniwersalnej odpowiedzi. Dla agresywnych celów, takich jak Google czy Amazon, 1-5 wniosków na minutę na IP jest bezpieczne. W przypadku słabo chronionych stron 20- 60 RPM na IP może działać. Należy zawsze rozpoczynać leczenie zachowawcze i zwiększać dawkę na podstawie obserwowanych wskaźników skuteczności.






