Jak scrapować eBay z proxy: praktyczny przewodnik dla resellerów i analityków

Kompletny przewodnik scrapowania eBay — od wyboru między API a HTML, przez selektory CSS, rotację proxy, aż po analizę aukcji i sprzedawców. Z przykładami kodu w Python.

Jak scrapować eBay z proxy: praktyczny przewodnik dla resellerów i analityków

API eBay czy scraping HTML — co wybrać?

Zanim napiszesz pierwszy request do eBay, musisz podjąć kluczową decyzję architektoniczną: oficjalne API czy parsowanie HTML? Odpowiedź zależy od skali, budżetu i tego, jakie dane potrzebujesz.

eBay oferuje dwa główne API: Finding API (wyszukiwanie) i Browse API (szczegóły listingu). Oba działają dobrze — dopóki nie uderzysz w limity.

Limity i quoty eBay API

  • Finding API: 5 000 wywołań/dzień na domyślnym tierze. Każdy request zwraca max 100 wyników na stronę, do 100 stron = 10 000 itemów/dzień.
  • Browse API: zależy od limitów aplikacji, typowo 1 500–5 000 wywołań/dzień. Wymaga autoryzacji OAuth.
  • Rate limiting: nagłówki X-RateLimit-Limit i X-RateLimit-Remaining w każdej odpowiedzi.
  • Proces aplikacji: rejestracja w eBay Developer Program, tworzenie aplikacji, oczekiwanie na approvement — od kilku godzin do kilku dni.

Problem? Jeśli monitorujesz 50 000 listingów dziennie w wielu kategoriach, API Ci nie wystarczy. I tu wchodzi scraping.

KryteriumeBay Finding/Browse APIScraping HTML
SkalowalnośćLimitowana quotami (5K–10K/dzień)Ograniczona tylko infrastrukturą proxy
Struktura danychJSON, stabilna schemaHTML, wymaga utrzymania parserów
Dostęp do danychTylko pola udostępnione przez APIWszystko co widoczne na stronie
OpóźnienieNiskie (~100ms)Średnie (~500ms + proxy latency)
Koszt infrastrukturyDarmowy do limitówKoszt proxy (residential ~$2-5/GB)
UtrzymanieMinimalneWymaga aktualizacji przy zmianach UI
BlokadyBrak (w ramach quota)Agresywne — IP bans, CAPTCHA

Reguła kciuka: jeśli potrzebujesz <10 000 itemów/dzień i wystarczą Ci standardowe pola — użyj API. Jeśli monitorujesz pełne kategorie, śledzisz aukcje w czasie rzeczywistym, albo potrzebujesz danych sprzedawców — scraping jest jedyną opcją.

Struktura HTML eBay — co i jak scrapować

eBay używa kilku powtarzalnych wzorców HTML. Zrozumienie ich jest kluczowe, zanim zaczniesz pisać parser.

Search results grid — .s-item

Strona wyników wyszukiwania (ebay.com/sch/i.html?_nkw=...) renderuje listę produktów w kontenerach z klasą .s-item. Każdy kontener zawiera:

  • .s-item__title — tytuł listingu
  • .s-item__price — cena (może zawierać „to" dla aukcji)
  • .s-item__shipping — koszt wysyłki („Free shipping", „+$5.99 shipping")
  • .s-item__bid-count — liczba ofert (tylko aukcje)
  • .s-item__time-left — czas do końca aukcji
  • .s-item__purchase-info — flaga Buy-It-Now
  • .s-item__seller-info — nazwa sprzedawcy
  • .s-item__image img — URL obrazka
  • .s-item__link — URL listingu

Selektory są stabilne od lat, ale eBay czasem dodaje A/B testy — zawsze loguj surowy HTML na próbce przed deploymentem.

Listing detail page

Pojedynczy listing (ebay.com/itm/...) zawiera znacznie więcej danych:

  • #vi-desc-maincntr — opis produktu (często iframe z zewnętrznego serwera)
  • #vi-tab-1-active — zakładka specyfikacji (Item Specifics)
  • .ux-labels-values__value-content — poszczególne atrybuty
  • #bidBtn — przycisk licytacji (obecny tylko przy aukcjach)
  • #binBtn — przycisk Buy-It-Now
  • .d-feedback — sekcja opinii o sprzedawcy

Seller profile page

Profil sprzedawcy (ebay.com/usr/...) zawiera:

  • .member-info — nazwa, data rejestracji
  • .feedback-score — wynik feedback (99.5% itp.)
  • .seller-items — liczba aktywnych listingów
  • Data z API: ebay.com/sch/m.html?_ssn=USERNAME&_pgn=1 — pełna lista produktów sprzedawcy

Proxy — dlaczego datacenter nie zadziała na eBay

eBay należy do najagresywniej broniących się przed botami platform e-commerce. Ich system anti-bot:

  • Fingerprinting TLS: eBay sprawdza JA3 fingerprint klienta — Python requests z defaultowym urllib wywoła flagę.
  • Behavioral analysis: zbyt szybkie nawigowanie między stronami, brak refererów, identyczne interwały między requestami.
  • Geo-IP matching: IP z Niemiec przeglądające eBay.com bez referera = podejrzane.
  • Rate limiting per IP: ~200–300 requestów z jednego IP w krótkim czasie wywołuje CAPTCHA (hCaptcha) lub tymczasowy ban.

Residential proxy — jedyny sensowny wybór

Residential proxy to IP prawdziwych urządzeń ISP. eBay nie może ich odróżnić od organicznego ruchu — dopóki nie przekroczysz rate limitu na pojedyncze IP.

Strategia rotacji:

  • Per-request rotation: nowy IP na każdy request — bezpieczne, ale droższe. Idealne do masowego scrapingu wyników wyszukiwania.
  • Sticky sessions: ten sam IP przez 10–30 minut — lepsze do scrapowania detail pages, gdzie zachowanie sesji wygląda naturalniej.

Geo-targeting dla regionalnych eBay

eBay.de, eBay.co.uk, eBay.com.au — każda domena zwraca inne wyniki i ceny. Używaj proxy z odpowiednim krajem:

# Niemcy — eBay.de z niemieckim IP
http://user-country-DE:PASSWORD@gate.proxyhat.com:8080

# UK — eBay.co.uk z brytyjskim IP
http://user-country-GB:PASSWORD@gate.proxyhat.com:8080

# USA — eBay.com z amerykańskim IP (domyślne)
http://user-country-US:PASSWORD@gate.proxyhat.com:8080

Bez geo-dopasowania eBay może przekierować Cię na inną domenę lub wyświetlić zawartość z innego regionu — co zepsuje Twoje dane.

Python: scraper wyników wyszukiwania z .s-item

Poniżej kompletny, działający przykład scrapowania wyników wyszukiwania eBay z rotacją proxy i parsowaniem .s-item do ustrukturyzowanych rekordów.

import requests
from bs4 import BeautifulSoup
import json
import time
import random

PROXY_URL = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY_URL, "https": PROXY_URL}

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/125.0.0.0 Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
}

def fetch_search_results(keyword, page=1):
    """Fetch eBay search results HTML for a given keyword and page."""
    url = "https://www.ebay.com/sch/i.html"
    params = {
        "_nkw": keyword,
        "_pgn": page,
        "_ipg": 60,  # items per page (max 240 for some categories)
    }
    try:
        resp = requests.get(
            url, params=params, headers=HEADERS,
            proxies=PROXIES, timeout=30
        )
        resp.raise_for_status()
        return resp.text
    except requests.RequestException as e:
        print(f"Request failed: {e}")
        return None

def parse_s_item(item_el):
    """Parse a single .s-item element into a structured record."""
    record = {}

    title_el = item_el.select_one(".s-item__title")
    record["title"] = title_el.get_text(strip=True) if title_el else None

    price_el = item_el.select_one(".s-item__price")
    record["price"] = price_el.get_text(strip=True) if price_el else None

    shipping_el = item_el.select_one(".s-item__shipping")
    record["shipping"] = shipping_el.get_text(strip=True) if shipping_el else None

    bid_el = item_el.select_one(".s-item__bid-count")
    record["bid_count"] = bid_el.get_text(strip=True) if bid_el else None

    time_el = item_el.select_one(".s-item__time-left")
    record["time_left"] = time_el.get_text(strip=True) if time_el else None

    bin_el = item_el.select_one(".s-item__purchase-info")
    record["buy_it_now"] = bin_el is not None

    link_el = item_el.select_one(".s-item__link")
    record["url"] = link_el["href"] if link_el else None

    # Extract item ID from URL
    if record["url"] and "/itm/" in record["url"]:
        parts = record["url"].split("/itm/")
        if len(parts) > 1:
            record["item_id"] = parts[1].split("?")[0]
        else:
            record["item_id"] = None
    else:
        record["item_id"] = None

    return record

def scrape_ebay_search(keyword, max_pages=3):
    """Scrape multiple pages of eBay search results."""
    all_records = []
    for page in range(1, max_pages + 1):
        html = fetch_search_results(keyword, page)
        if not html:
            break
        soup = BeautifulSoup(html, "html.parser")
        items = soup.select(".s-item")
        # Skip first item — eBay injects a promotional tile
        for item in items[1:]:
            record = parse_s_item(item)
            record["keyword"] = keyword
            record["page"] = page
            all_records.append(record)
        # Random delay to appear human
        time.sleep(random.uniform(2, 5))
    return all_records

if __name__ == "__main__":
    results = scrape_ebay_search("vintage rolex", max_pages=2)
    print(json.dumps(results[:3], indent=2))

Przykładowy wynik (skrócony):

[
  {
    "title": "Vintage Rolex Submariner 5513 1967",
    "price": "$8,500.00",
    "shipping": "Free shipping",
    "bid_count": null,
    "time_left": null,
    "buy_it_now": true,
    "url": "https://www.ebay.com/itm/1234567890?...",
    "item_id": "1234567890",
    "keyword": "vintage rolex",
    "page": 1
  },
  {
    "title": "Rolex Datejust 36 Vintage 1980s",
    "price": "$4,200.00",
    "shipping": "+$25.00 shipping",
    "bid_count": "12 bids",
    "time_left": "2h 15m left",
    "buy_it_now": false,
    "url": "https://www.ebay.com/itm/9876543210?...",
    "item_id": "9876543210",
    "keyword": "vintage rolex",
    "page": 1
  }
]

Obsługa aukcji — czas, bidy, Buy-It-Now

Aukcje to najcenniejsze dane dla resellerów — cena końcowa zależy od dynamiki licytacji. Oto jak wyodrębnić kluczowe pola.

Czas do końca aukcji

eBay wyświetla czas w formacie relatywnym na stronie wyników ("2h 15m left") i jako precyzyjny timestamp na stronie detail:

from datetime import datetime, timezone
import re

def extract_auction_end_time(detail_html):
    """Extract precise auction end time from listing detail page."""
    soup = BeautifulSoup(detail_html, "html.parser")
    
    # Method 1: time element with ISO timestamp
    time_el = soup.select_one("time#vi-cdown_time")
    if time_el and time_el.get("datetime"):
        return datetime.fromisoformat(
            time_el["datetime"].replace("Z", "+00:00")
        )
    
    # Method 2: parse from inline script (tmd=...)
    script_tags = soup.find_all("script")
    for script in script_tags:
        match = re.search(r'"endTime":"(\d{4}-\d{2}-\d{2}T[^"]+)"', script.string or "")
        if match:
            return datetime.fromisoformat(match.group(1))
    
    return None

def parse_time_left(time_str):
    """Parse relative time string like '2h 15m left' into minutes."""
    if not time_str:
        return None
    total_minutes = 0
    days = re.search(r'(\d+)d', time_str)
    hours = re.search(r'(\d+)h', time_str)
    mins = re.search(r'(\d+)m', time_str)
    if days:
        total_minutes += int(days.group(1)) * 1440
    if hours:
        total_minutes += int(hours.group(1)) * 60
    if mins:
        total_minutes += int(mins.group(1))
    return total_minutes if total_minutes > 0 else None

def classify_auction_listing(record):
    """Classify a listing as auction, BIN, or both."""
    is_auction = record.get("bid_count") is not None or record.get("time_left") is not None
    is_bin = record.get("buy_it_now")
    
    if is_auction and is_bin:
        return "auction_with_bin"
    elif is_auction:
        return "auction"
    elif is_bin:
        return "fixed_price"
    else:
        return "unknown"

Dlaczego aukcje wymagają częstszej rotacji IP

Monitorowanie aukcji wymaga odświeżania co kilka minut — szczególnie w ostatniej godzinie („sniping"). To oznacza 10–20x więcej requestów per listing niż przy jednorazowym skanie. Używaj sticky sessions z czasem życia 15–30 minut, żeby zachować ciągłość sesji i nie wyglądać podejrzanie:

# Sticky session — ten sam IP na 30 minut
PROXY_URL = "http://user-session-auction42:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY_URL, "https": PROXY_URL}

def monitor_auction(item_id, interval_seconds=120, max_checks=30):
    """Monitor an auction listing at regular intervals."""
    url = f"https://www.ebay.com/itm/{item_id}"
    snapshots = []
    
    for i in range(max_checks):
        try:
            resp = requests.get(
                url, headers=HEADERS, proxies=PROXIES, timeout=30
            )
            soup = BeautifulSoup(resp.text, "html.parser")
            
            bid_el = soup.select_one("#prcIsum_bid")
            bid_count_el = soup.select_one("#bidCount")
            time_el = soup.select_one("time#vi-cdown_time")
            
            snapshot = {
                "item_id": item_id,
                "current_bid": bid_el.get_text(strip=True) if bid_el else None,
                "bid_count": bid_count_el.get_text(strip=True) if bid_count_el else None,
                "end_time": time_el.get("datetime") if time_el else None,
                "check_number": i + 1,
            }
            snapshots.append(snapshot)
            print(f"Check {i+1}: {snapshot['current_bid']} ({snapshot['bid_count']})")
            
            time.sleep(interval_seconds + random.uniform(-10, 10))
        except Exception as e:
            print(f"Check {i+1} failed: {e}")
            time.sleep(interval_seconds)
    
    return snapshots

Analiza sprzedawców — feedback, kategorie, cross-listing

Dla reseller-intel i market-research teams, dane o sprzedawcach są równie cenne jak dane o produktach. Pozwalają identyfikować trendy, szacować wolumeny i wykrywać konta dropshippingowe.

Kluczowe metryki sprzedawcy

  • Feedback score: liczba w .feedback-score — historyczny wynik (0–100%)
  • Feedback volume: ostatnie 12 miesięcy — wskazuje na realny wolumen sprzedaży
  • Category distribution: w jakich kategoriach sprzedawca jest aktywny
  • Cross-listing patterns: czy ten sam item pojawia się u wielu sprzedawców (dropshipping signal)
def scrape_seller_items(username, max_pages=5):
    """Scrape all active listings for a seller via their store page."""
    all_items = []
    
    for page in range(1, max_pages + 1):
        # Rotate proxy per page for large sellers
        proxy = f"http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
        proxies = {"http": proxy, "https": proxy}
        
        url = "https://www.ebay.com/sch/m.html"
        params = {
            "_ssn": username,
            "_pgn": page,
            "_ipg": 60,
        }
        
        try:
            resp = requests.get(
                url, params=params, headers=HEADERS,
                proxies=proxies, timeout=30
            )
            resp.raise_for_status()
        except requests.RequestException as e:
            print(f"Page {page} failed: {e}")
            break
        
        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select(".s-item")
        
        if not items or len(items) <= 1:  # no results or promo-only
            break
        
        for item in items[1:]:
            record = parse_s_item(item)
            record["seller"] = username
            all_items.append(record)
        
        time.sleep(random.uniform(3, 7))
    
    return all_items

def analyze_seller_portfolio(items):
    """Analyze a seller's listing portfolio for patterns."""
    total = len(items)
    auction_count = sum(1 for i in items if i.get("bid_count") is not None)
    bin_count = sum(1 for i in items if i.get("buy_it_now"))
    
    # Price distribution
    prices = []
    for i in items:
        price_str = i.get("price", "")
        if price_str:
            # Handle "$1,234.56" format
            cleaned = price_str.replace("$", "").replace(",", "").split(" to ")[0]
            try:
                prices.append(float(cleaned))
            except ValueError:
                pass
    
    avg_price = sum(prices) / len(prices) if prices else 0
    
    return {
        "total_listings": total,
        "auction_listings": auction_count,
        "fixed_price_listings": bin_count,
        "auction_ratio": round(auction_count / total, 2) if total > 0 else 0,
        "avg_price": round(avg_price, 2),
        "min_price": min(prices) if prices else None,
        "max_price": max(prices) if prices else None,
    }

Wykrywanie cross-listingu i dropshippingu

Porównaj tytuły i zdjęcia wielu sprzedawców w tej samej kategorii. Jeśli 3+ sprzedawców listuje identyczny produkt z tą samą ceną — to prawdopodobnie dropshipping z tego samego źródła (często AliExpress).

Technika: hashowanie tytułów (normalizacja + SimHash) i porównanie odległości Hamminga. Wynik <3 bity = prawdopodobny duplicate.

Anti-bot eBay — co Cię czeka i jak to obejść

eBay używa wielowarstwowego systemu ochrony:

  • hCaptcha: pojawia się po ~200–300 requestach z jednego IP. Rozwiązanie: zmień IP przez residential proxy.
  • JavaScript challenges: niektóre strony wymagają wykonania JS. Rozwiązanie: Playwright/Puppeteer z residential proxy.
  • TLS fingerprinting: eBay sprawdza JA3 hash. Rozwiązanie: użyj curl_cffi lub tls_client zamiast czystego requests.
  • Cookie-based tracking: eBay śledzi sesje przez cookies. Rozwiązanie: czyść cookies między sesjami lub używaj fresh proxy per session.

Pragmatyczne podejście: jeśli Twój scraper dostaje CAPTCHĘ na >10% requestów, musisz spowolnić tempo lub zwiększyć pulę IP. Residential proxy z rotacją per-request rozwiązuje 95% problemów.

Najlepsze praktyki i optymalizacja

  • Limit concurrency: max 5–10 równoległych requestów per proxy session. Więcej = szybszy ban.
  • Randomizuj opóźnienia: random.uniform(2, 6) sekund między requestami. Ludzie nie klikają co równe 3.0s.
  • Używaj realistycznych nagłówków: pełny zestaw Accept, Accept-Language, Referer. Brak Referera = red flag.
  • Monitoruj success rate: jeśli spada poniżej 90%, zmień strategię rotacji proxy.
  • Cache aggressively: listingi, które się nie zmieniły, nie wymagają re-scrapowania. Użyj ETag / Last-Modified.
  • Respect robots.txt: eBay's robots.txt blokuje /sch/ dla botów — technicznie możesz to obejść, ale miej świadomość etycznych i prawnych implikacji.

Uwaga prawna: Scraping eBay może naruszać ich Terms of Service. Upewnij się, że Twoje działania są zgodne z obowiązującym prawem (GDPR, CCPA) i regulaminem platformy. Ten artykuł ma charakter edukacyjny — nie zachęcamy do łamania regulaminów.

Kluczowe wnioski

  • API vs scraping: używaj eBay API do <10K itemów/dzień; scraping dla pełnych kategorii, aukcji i danych sprzedawców.
  • Residential proxy jest obowiązkowe — eBay blokuje datacenter IP agresywnie. Geo-targeting jest niezbędny dla regionalnych domen.
  • Selektory .s-item są stabilne i pokrywają 90% danych z wyników wyszukiwania. Detail pages wymagają dodatkowego parsowania.
  • Aukcje wymagają sticky sessions i częstszego odświeżania — planuj budżet proxy odpowiednio.
  • Analiza sprzedawców to ukryty goldmine — portfolio, feedback i cross-listing patterns dają przewagę nad konkurencją.
  • Anti-bot: hCaptcha po ~200 requestach/IP, TLS fingerprinting, JS challenges — residential proxy + rotacja rozwiązuje większość problemów.

Gotowy do scrapowania eBay na skalę? Sprawdź plany ProxyHat — residential proxy z geo-targetingiem w 190+ krajach, gotowe do integracji z Twoim scraperem.

Gotowy, aby zacząć?

Dostęp do ponad 50 mln rezydencjalnych IP w ponad 148 krajach z filtrowaniem AI.

Zobacz cenyProxy rezydencjalne
← Powrót do Bloga