Jak scrapować REST API Binance z proxy — przewodnik dla programistów

Kompletny przewodnik po pobieraniu publicznych danych rynkowych z REST API Binance z wykorzystaniem rotacyjnych proxy. Przykłady kodu w Python i Node.js, limit wag, sesje sticky i strategie omijania blokad IP.

How to Scrape the Binance REST API with Proxies: A Developer Guide
W tym artykule

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

EndpointOpisWaga (domyślna)Kluczowe parametry
GET /api/v3/klinesŚwiece OHLCV1–2 (zależnie od limit)symbol, interval, limit (max 1000)
GET /api/v3/depthKsięga zleceń5–20 (zależnie od limit: 5, 10, 20, 50, 100, 500, 1000)symbol, limit
GET /api/v3/ticker/24hrStatystyki 24h1 (pojedynczy symbol), 40 (wszystkie)symbol (opcjonalny)
GET /api/v3/ticker/priceAktualna cena1 (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@depth aktualizuje orderbook z opóźnieniem <50ms, podczas gdy REST polling co 1s zużywa 20 wag/s.
  • Ciągłe klines: strumień @kline_1m eliminuje potrzebę pollingu /api/v3/klines dla danych live.
  • Ticker all-market: !ticker@arr dostarcza 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.us jeśli dotyczy.

ProxyHat vs inne typy proxy — porównanie

CechaResidential (ProxyHat)DatacenterMobile
Wykrywalność przez BinanceNiskaWysokaBardzo niska
KosztŚredniNiskiWysoki
Rotacja IPPer-request lub stickyManualnaPer-request
Geo-targetingKraj + miastoOgraniczonyKraj
Niezawodność dla API BinanceWysokaŚ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

  1. Monitoruj X-MBX-USED-WEIGHT-1M przy każdym żądaniu i throttle proaktywnie przy >5000.
  2. Używaj rotacji per-request dla pollingu depth/ticker i sticky session dla paginacji klines.
  3. Implementuj exponential backoff z jitter przy 429/418/timeout.
  4. Ogranicz współbieżność do 5–20 jednoczesnych żądań w zależności od budżetu wagi.
  5. Loguj zużycie wagi do metryk (Prometheus/Datadog) dla alertowania.
  6. Preferuj WebSocket dla danych live, REST dla backfill.
  7. 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-US omija 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.

Często zadawane pytania

Czym jest scrapowanie REST API Binance z proxy?

Scrapowanie REST API Binance z proxy to technika pobierania publicznych danych rynkowych (klines, depth, tickery) z endpointów api.binance.com przy użyciu rotacyjnych serwerów proxy, które rozpraszają żądania na wiele adresów IP. Pozwala to na ominięcie limitów wagowych Binance (ok. 6000 wag/min na IP) i uniknięcie blokad 429/418. Proxy residential z ProxyHat (gate.proxyhat.com:8080) oferują rotację per-request i sesje sticky dla paginacji.

Dlaczego scrapowanie REST API Binance z proxy jest ważne dla użytkowników proxy?

Binance stosuje limit oparty na wadze (weight), nie na liczbie żądań. Endpoint /api/v3/depth z limitem 1000 kosztuje 20 wag — polling 10 par co 500ms zużywa 2400 wag/s, wyczerpując budżet 6000 w 2,5 sekundy. Bez rotacji IP serwer zwraca 429, a następnie 418 (ban IP). Rotacyjne proxy residential rozpraszają wagę na wiele IP, umożliwiając skalowanie zbierania danych bez blokad.

Który typ proxy najlepiej działa do scrapowania REST API Binance?

Rotacyjne proxy residential są optymalne do scrapowania REST API Binance. Mają niską wykrywalność (są trudniejsze do zidentyfikowania jako datacenter), pełną rotację IP per-request lub sesje sticky dla paginacji, oraz geo-targeting na poziomie kraju i miasta. Proxy datacenter są tańsze, ale mają wysoką wykrywalność i ryzyko bana. Proxy mobile są najbardziej niezawodne, ale najdroższe. ProxyHat oferuje residential przez gate.proxyhat.com:8080 (HTTP) i :1080 (SOCKS5).

Jak uniknąć blokad przy scrapowaniu REST API Binance?

Aby uniknąć blokad: (1) monitoruj nagłówek X-MBX-USED-WEIGHT-1M i throttle proaktywnie przy wadze >5000; (2) używaj rotacji IP per-request dla pollingu i sesji sticky dla paginacji; (3) implementuj exponential backoff z jitter przy 429; (4) zawsze szanuj nagłówek Retry-After — kontynuacja żądań po 429 prowadzi do 418; (5) ogranicz współbieżność do 5–20 jednoczesnych żądań; (6) preferuj WebSocket dla danych live zamiast REST polling.

Sprawdź konfigurację proxy w kilka sekund

Darmowy tester proxy — potwierdź, że Twoje IP są szybkie, anonimowe i nieblokowane.

Sprawdź proxy za darmo
← Powrót do Bloga