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-LimitiX-RateLimit-Remainingw 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.
| Kryterium | eBay Finding/Browse API | Scraping HTML |
|---|---|---|
| Skalowalność | Limitowana quotami (5K–10K/dzień) | Ograniczona tylko infrastrukturą proxy |
| Struktura danych | JSON, stabilna schema | HTML, wymaga utrzymania parserów |
| Dostęp do danych | Tylko pola udostępnione przez API | Wszystko co widoczne na stronie |
| Opóźnienie | Niskie (~100ms) | Średnie (~500ms + proxy latency) |
| Koszt infrastruktury | Darmowy do limitów | Koszt proxy (residential ~$2-5/GB) |
| Utrzymanie | Minimalne | Wymaga aktualizacji przy zmianach UI |
| Blokady | Brak (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_cffilubtls_clientzamiast czystegorequests. - 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-itemsą 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.






