Jak scrapować REST API Binance z proxy — wprowadzenie
Binance udostępnia publiczne endpointy REST API do pobierania danych rynkowych w czasie rzeczywistym — świece (klines), księga zleceń (depth) i tickery. Dla quant-developerów i inżynierów danych kryptowalutowych te endpointy to podstawowe źródło do budowy historycznych zbiorów cen i orderbooków. Problem pojawia się przy skalowaniu: Binance stosuje limit oparty na wadze (weight) na adres IP — około 6000 wag/min — a jego przekroczenie prowadzi do błędów 429, a następnie do banów IP (HTTP 418). W tym przewodniku pokazujemy, jak scrapować REST API Binance z proxy, rozpraszając wagę na wiele adresów IP przy użyciu ProxyHat.
Ważne o zgodności: Ten artykuł dotyczy wyłącznie publicznych danych rynkowych z publicznych endpointów Binance. Przestrzegaj warunków korzystania z Binance oraz dokumentacji API. Preferuj oficjalny dostęp API, gdy wymaga tego umowa. Nie pobieraj danych prywatnych ani nie obchodź mechanizmów autoryzacji.
Architektura limitów wagowych Binance — dlaczego problem w ogóle istnieje
Binance nie ogranicza liczby żądań, ale sumy wag przypisanych do endpointów. Każde żądanie zużywa określoną wagę z budżetu IP. Gdy budżet zostanie wyczerpany, serwer zwraca 429 Too Many Requests. Kontynuowanie żądań po 429 prowadzi do tymczasowego bana — 418 — z nagłówkiem Retry-After określającym czas trwania bana.
Budżet wynosi około 6000 wag na minutę na adres IP. Kluczowy nagłówek odpowiedzi to X-MBX-USED-WEIGHT-1M, który informuje o aktualnie zużytej wadze w ostatnich 60 sekundach. Monitorowanie tego nagłówka to podstawa skutecznego throttlingu.
Mapa kluczowych endpointów publicznych
| Endpoint | Opis | Waga (domyślna) | Kluczowe parametry |
|---|---|---|---|
GET /api/v3/klines | Świece OHLCV | 1–2 (zależnie od limit) | symbol, interval, limit (max 1000) |
GET /api/v3/depth | Księga zleceń | 5–20 (zależnie od limit: 5, 10, 20, 50, 100, 500, 1000) | symbol, limit |
GET /api/v3/ticker/24hr | Statystyki 24h | 1 (pojedynczy symbol), 40 (wszystkie) | symbol (opcjonalny) |
GET /api/v3/ticker/price | Aktualna cena | 1 (pojedyńczy), 2 (wszystkie) | symbol (opcjonalny) |
Dlaczego /api/v3/depth z wysokim limitem wyczerpuje budżet w minuty? Przy limicie 1000 waga wynosi 20. Polling co 1 sekundę = 20 wag/s = 1200 wag/min — to 20% budżetu na jeden symbol. Przy 10 parach i pollingu co 500 ms zużywasz 2400 wag/s, czyli budżet 6000 znika w ~2,5 sekundy. Bez rotacji IP jest to niepraktyczne.
Geo-split Binance.com vs Binance.US i HTTP 451
Binance operuje osobnymi domenami dla różnych jurysdykcji. Użytkownicy z USA próbujący dostać się do api.binance.com mogą otrzymać 451 Unavailable For Legal Reasons. Rotacyjne proxy residential z geo-targetingiem -country-US pozwalają na dostęp z amerykańskich adresów IP, ale należy pamiętać, że api.binance.us to osobne API z własnymi limitami. Używaj geo-targetowania zgodnie z obowiązującymi przepisami.
Konfiguracja ProxyHat — rotacja IP i sesje sticky
ProxyHat oferuje rotacyjne proxy residential przez bramkę gate.proxyhat.com. HTTP działa na porcie 8080, SOCKS5 na 1080. Parametry geo-targeting i sesji przekazywane są w nazwie użytkownika:
- Rotacja per-request:
http://user:pass@gate.proxyhat.com:8080 - Geo-targeting US:
http://user-country-US:pass@gate.proxyhat.com:8080 - Sesja sticky:
http://user-session-abc123:pass@gate.proxyhat.com:8080 - Geo + miasto:
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080
Sprawdź dostępne lokalizacje na stronie /pl/locations oraz cennik na /pl/pricing.
Przykład 1: Python requests — rotacja IP per-request (surowe proxy)
Poniższy przykład pobiera świece z /api/v3/klines, rotując IP przy każdym żądaniu. Zawiera obsługę nagłówka X-MBX-USED-WEIGHT-1M, exponential backoff przy 429 i circuit breaker przy 418.
import requests
import time
import random
import logging
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("binance-scraper")
PROXY_URL = "http://user:pass@gate.proxyhat.com:8080"
BASE_URL = "https://api.binance.com"
def fetch_klines(symbol: str, interval: str = "1m", limit: int = 1000,
max_retries: int = 5) -> Optional[list]:
"""Pobiera klines z rotacją IP i weight-aware throttling."""
url = f"{BASE_URL}/api/v3/klines"
params = {"symbol": symbol, "interval": interval, "limit": limit}
proxies = {"http": PROXY_URL, "https": PROXY_URL}
for attempt in range(max_retries):
try:
resp = requests.get(url, params=params, proxies=proxies, timeout=15)
# Monitoruj zużytą wagę
used_weight = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", 0))
logger.info(f"{symbol} attempt={attempt} weight={used_weight} status={resp.status_code}")
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logger.warning(f"429 received, sleeping {retry_after}s")
time.sleep(retry_after + random.uniform(0.5, 1.5))
continue
if resp.status_code == 418:
logger.error("IP banned (418). Rotating to next IP...")
time.sleep(60 + random.uniform(10, 30))
continue
resp.raise_for_status()
# Proactive throttle: jeśli waga > 5000, odczekaj
if used_weight > 5000:
sleep_time = (used_weight - 5000) / 100 # ~0.1s per 100 wagi powyżej 5000
time.sleep(sleep_time)
return resp.json()
except requests.RequestException as e:
backoff = (2 ** attempt) + random.uniform(0, 1)
logger.warning(f"Request error: {e}, backing off {backoff:.1f}s")
time.sleep(backoff)
return None
# Pobierz 1000 świec 1m dla BTCUSDT
data = fetch_klines("BTCUSDT", "1m", 1000)
if data:
print(f"Pobrano {len(data)} świec")
Przykład 2: ProxyHat SDK — rotacja IP per-request
ProxyHat SDK upraszcza zarządzanie proxy, automatyzując rotację i ponowne próby. Poniżej to samo zadanie co w Przykładzie 1, ale z użyciem SDK:
from proxyhat import ProxyHatClient
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("proxyhat-binance")
# Inicjalizacja klienta ProxyHat
client = ProxyHatClient(
host="gate.proxyhat.com",
port=8080,
username="user",
password="pass",
rotate_every_request=True, # rotacja IP przy każdym żądaniu
)
BASE_URL = "https://api.binance.com"
def fetch_klines_sdk(symbol: str, interval: str = "1m", limit: int = 1000) -> list:
"""Pobiera klines używając ProxyHat SDK z automatyczną rotacją IP."""
url = f"{BASE_URL}/api/v3/klines"
params = {"symbol": symbol, "interval": interval, "limit": limit}
response = client.get(url, params=params, timeout=15)
used_weight = int(response.headers.get("X-MBX-USED-WEIGHT-1M", 0))
logger.info(f"{symbol} weight={used_weight} status={response.status_code}")
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 5))
logger.warning(f"Rate limited, retrying after {retry_after}s")
return client.get_with_retry(url, params=params, retries=3,
backoff_base=retry_after)
response.raise_for_status()
return response.json()
# Backfill wielu par
symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT"]
for sym in symbols:
klines = fetch_klines_sdk(sym, "1m", 1000)
logger.info(f"{sym}: {len(klines)} świec")
Przykład 3: Sticky session — paginated klines backfill
Backfill historycznych danych wymaga wielu stronicowanych żądań. Sticky session utrzymuje ten sam IP dla całej paginacji, co zapobiega rotacji w środku sekwencji i zmniejsza ryzyko 429 na pojedynczym IP.
import requests
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("klines-backfill")
BASE_URL = "https://api.binance.com"
SESSION_ID = "backfill-btc-001"
# Sticky session — ten sam IP przez całą sekwencję
PROXY_URL = f"http://user-session-{SESSION_ID}:pass@gate.proxyhat.com:8080"
proxies = {"http": PROXY_URL, "https": PROXY_URL}
def backfill_klines(symbol: str, interval: str, start_time_ms: int,
end_time_ms: int, max_calls: int = 1000) -> list:
"""Paginated backfill klines używając sticky session."""
all_klines = []
current_start = start_time_ms
calls = 0
while current_start < end_time_ms and calls < max_calls:
params = {
"symbol": symbol,
"interval": interval,
"startTime": current_start,
"endTime": end_time_ms,
"limit": 1000,
}
resp = requests.get(f"{BASE_URL}/api/v3/klines", params=params,
proxies=proxies, timeout=15)
used_weight = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", 0))
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logger.warning(f"429 at call {calls}, sleeping {retry_after}s")
time.sleep(retry_after + 1)
continue
if resp.status_code != 200:
logger.error(f"HTTP {resp.status_code}: {resp.text[:200]}")
time.sleep(2)
continue
batch = resp.json()
if not batch:
break
all_klines.extend(batch)
# Następny start = timestamp ostatniej świecy + 1ms
current_start = batch[-1][0] + 1
calls += 1
# Weight-aware throttle
if used_weight > 4500:
sleep_sec = max(1, (used_weight - 4500) / 200)
logger.info(f"Weight {used_weight}, throttling {sleep_sec:.1f}s")
time.sleep(sleep_sec)
else:
time.sleep(0.1) # minimalny delay
if calls % 50 == 0:
logger.info(f"{symbol}: {len(all_klines)} klines, calls={calls}")
return all_klines
# Backfill 30 dni BTCUSDT 1m
import datetime
end_ms = int(datetime.datetime(2024, 1, 1).timestamp() * 1000)
start_ms = int(datetime.datetime(2023, 12, 1).timestamp() * 1000)
klines = backfill_klines("BTCUSDT", "1m", start_ms, end_ms)
print(f"Łącznie pobrano: {len(klines)} świec")
Przykład 4: Python httpx — concurrency z async i rotacją IP
Dla pobierania wielu par jednocześnie, httpx z async pozwala na kontrolowaną współbieżność z osobnym limiterem:
import httpx
import asyncio
import logging
from typing import Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("async-binance")
BASE_URL = "https://api.binance.com"
PROXY_URL = "http://user:pass@gate.proxyhat.com:8080"
# Limit współbieżności — nie więcej niż 10 jednoczesnych żądań
semaphore = asyncio.Semaphore(10)
class WeightTracker:
"""Śledzi zużycie wagi w oknie 1-minutowym."""
def __init__(self, max_weight: int = 5500):
self.max_weight = max_weight
self.current_weight = 0
async def check_and_wait(self, client: httpx.AsyncClient):
if self.current_weight >= self.max_weight:
wait = 60 # odczekaj pełne okno
logger.info(f"Weight {self.current_weight}, waiting {wait}s")
await asyncio.sleep(wait)
self.current_weight = 0
def update(self, weight_header: str):
if weight_header:
self.current_weight = int(weight_header)
weight_tracker = WeightTracker(max_weight=5500)
async def fetch_ticker(client: httpx.AsyncClient, symbol: str) -> Optional[dict]:
async with semaphore:
await weight_tracker.check_and_wait(client)
url = f"{BASE_URL}/api/v3/ticker/24hr"
params = {"symbol": symbol}
try:
resp = await client.get(url, params=params, timeout=15)
weight_tracker.update(resp.headers.get("X-MBX-USED-WEIGHT-1M"))
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 5))
logger.warning(f"{symbol}: 429, sleeping {retry_after}s")
await asyncio.sleep(retry_after)
return None
resp.raise_for_status()
data = resp.json()
logger.info(f"{symbol}: price={data['lastPrice']} weight={weight_tracker.current_weight}")
return data
except Exception as e:
logger.error(f"{symbol}: {e}")
return None
async def main():
symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT",
"ADAUSDT", "DOGEUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT"]
async with httpx.AsyncClient(
proxy=PROXY_URL,
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10)
) as client:
tasks = [fetch_ticker(client, sym) for sym in symbols]
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = [r for r in results if r is not None and isinstance(r, dict)]
print(f"Pobrano {len(successful)}/{len(symbols)} tickerów")
asyncio.run(main())
Przykład 5: Node.js axios — rotacja IP i exponential backoff
Dla ekosystemu JavaScript/TypeScript, poniższy przykład używa axios z HttpsProxyAgent i implementuje exponential backoff:
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const pLimit = require('p-limit');
const PROXY_URL = 'http://user:pass@gate.proxyhat.com:8080';
const BASE_URL = 'https://api.binance.com';
const proxyAgent = new HttpsProxyAgent(PROXY_URL);
// Limit współbieżności: max 5 jednoczesnych
const limit = pLimit(5);
// Weight tracker
let currentWeight = 0;
async function fetchDepth(symbol, limitParam = 100) {
const url = `${BASE_URL}/api/v3/depth`;
const params = { symbol, limit: limitParam };
for (let attempt = 0; attempt < 5; attempt++) {
try {
const resp = await axios.get(url, {
params,
httpsAgent: proxyAgent,
timeout: 15000,
headers: { 'User-Agent': 'MarketDataBot/1.0' },
});
currentWeight = parseInt(resp.headers['x-mbx-used-weight-1m'] || '0', 10);
console.log(`${symbol} weight=${currentWeight} status=${resp.status}`);
// Proactive throttle
if (currentWeight > 5000) {
const sleepMs = Math.max(1000, (currentWeight - 5000) * 10);
console.log(`Throttling ${sleepMs}ms`);
await new Promise(r => setTimeout(r, sleepMs));
}
return resp.data;
} catch (err) {
if (err.response) {
const status = err.response.status;
const retryAfter = err.response.headers['retry-after'];
if (status === 429) {
const wait = (parseInt(retryAfter || '5', 10)) * 1000;
console.warn(`${symbol}: 429, waiting ${wait}ms`);
await new Promise(r => setTimeout(r, wait + 500));
continue;
}
if (status === 418) {
console.error(`${symbol}: IP banned (418). Waiting 120s.`);
await new Promise(r => setTimeout(r, 120000));
continue;
}
}
// Exponential backoff
const backoff = Math.pow(2, attempt) * 1000 + Math.random() * 500;
console.warn(`${symbol}: attempt ${attempt}, backoff ${backoff}ms, error: ${err.message}`);
await new Promise(r => setTimeout(r, backoff));
}
}
return null;
}
async function main() {
const symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT'];
const tasks = symbols.map(sym =>
limit(() => fetchDepth(sym, 100))
);
const results = await Promise.allSettled(tasks);
const successful = results.filter(r => r.status === 'fulfilled' && r.value !== null);
console.log(`Pobrano ${successful.length}/${symbols.length} orderbooków`);
}
main().catch(console.error);
Przykład 6: curl — szybkie testowanie proxy
Najprostszy sposób na weryfikację działania proxy:
# Test połączenia przez proxy HTTP
curl -x http://user:pass@gate.proxyhat.com:8080 \
"https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT" \
-H "Accept: application/json" \
-D -
# Test geo-targeting US
curl -x http://user-country-US:pass@gate.proxyhat.com:8080 \
"https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"
# Test SOCKS5
curl -x socks5://user:pass@gate.proxyhat.com:1080 \
"https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=100"
Kiedy WebSocket bije REST polling
Binance oferuje publiczne strumienie WebSocket (wss://stream.binance.com:9443) dla klines, depth i tickerów. Kiedy warto ich używać zamiast REST?
- Depth w czasie rzeczywistym:
wss://stream.binance.com:9443/ws/btcusdt@depthaktualizuje orderbook z opóźnieniem <50ms, podczas gdy REST polling co 1s zużywa 20 wag/s. - Ciągłe klines: strumień
@kline_1meliminuje potrzebę pollingu/api/v3/klinesdla danych live. - Ticker all-market:
!ticker@arrdostarcza tickery wszystkich par w jednym strumieniu, oszczędzając setki żądań REST.
REST pozostaje lepszy dla: backfill danych historycznych (WebSocket nie wysyła danych z przeszłości), jednorazowych zapytań i operacji wymagających deterministycznych odpowiedzi.
Najczęstsze błędy i przypadki brzegowe
- Ignorowanie nagłówka X-MBX-USED-WEIGHT-1M: to jedyny sposób na proaktywne throttle. Bez monitorowania wagi, 429 jest nieuniknione.
- Zbyt wysoki limit na /api/v3/depth: limit=1000 kosztuje 20 wag. Przy 10 parach i pollingu co 500ms = 2400 wag/s — budżet 6000 znika w 2,5s.
- Brak Retry-After przy 429: kontynuacja żądań po 429 prowadzi do 418 (ban IP). Zawsze szanuj nagłówek Retry-After.
- Sticky session dla pollingu: sticky session przy ciągłym pollingu depth = ten sam IP zużywa pełny budżet. Rotacja per-request jest lepsza dla pollingu, sticky — dla paginacji.
- Brak User-Agent: Binance może odrzucać żądania bez poprawnego nagłówka User-Agent.
- HTTP 451: geo-blokada. Użyj geo-targeting lub przełącz na
api.binance.usjeśli dotyczy.
ProxyHat vs inne typy proxy — porównanie
| Cecha | Residential (ProxyHat) | Datacenter | Mobile |
|---|---|---|---|
| Wykrywalność przez Binance | Niska | Wysoka | Bardzo niska |
| Koszt | Średni | Niski | Wysoki |
| Rotacja IP | Per-request lub sticky | Manualna | Per-request |
| Geo-targeting | Kraj + miasto | Ograniczony | Kraj |
| Niezawodność dla API Binance | Wysoka | Średnia (ryzyko bana) | Wysoka |
Residential proxy z ProxyHat to optymalny wybór dla scrapowania Binance — niska wykrywalność, pełna rotacja i geo-targeting. Zobacz przypadek użycia web scraping oraz SERP tracking dla więcej scenariuszy.
Najlepsze praktyki produkcyjne
- Monitoruj X-MBX-USED-WEIGHT-1M przy każdym żądaniu i throttle proaktywnie przy >5000.
- Używaj rotacji per-request dla pollingu depth/ticker i sticky session dla paginacji klines.
- Implementuj exponential backoff z jitter przy 429/418/timeout.
- Ogranicz współbieżność do 5–20 jednoczesnych żądań w zależności od budżetu wagi.
- Loguj zużycie wagi do metryk (Prometheus/Datadog) dla alertowania.
- Preferuj WebSocket dla danych live, REST dla backfill.
- Respektuj robots.txt i ToS Binance — publiczne dane rynkowe tylko.
Kluczowe wnioski
- Limit Binance to waga, nie liczba żądań — około 6000 wag/min na IP. Monitoruj
X-MBX-USED-WEIGHT-1M.- Rotacja residential proxy ProxyHat rozprasza wagę na wiele IP, eliminując ryzyko 429/418.
- Sesje sticky (
-session-abc123) są optymalne dla paginacji klines — stabilny IP przez całą sekwencję.- Geo-targeting
-country-USomija HTTP 451 przy dostępie z USA.- WebSocket bije REST polling dla danych live (depth, klines, tickery).
- Zgodność: tylko publiczne dane rynkowe, przestrzegaj ToS i limitów wagowych.
Zacznij zbierać dane rynkowe Binance bez blokad IP — sprawdź cennik ProxyHat i skonfiguruj pierwsze proxy w kilka minut. Pełna dokumentacja dostępna jest na docs.proxyhat.com.






