Scraper eBay avec des proxies : guide pragmatique pour équipes de market research

Comparez les API eBay au scraping HTML, maîtrisez les sélecteurs .s-item, gérez les enchères et l'analytique vendeur avec des proxies résidentiels geo-ciblés. Code Python inclus.

Scraper eBay avec des proxies : guide pragmatique pour équipes de market research

API eBay vs scraping HTML : le compromis fondamental

Si vous cherchez à scrapeer eBay à grande échelle, la première décision est architecturale : utiliser les API officielles d'eBay ou extraire directement le HTML. Les deux approches ont leur place, mais le choix dépend de vos quotas, de la fraîcheur des données et de la couverture que vous visez.

eBay propose trois API principales pour les développeurs :

  • Finding API — recherche et filtres avancés sur les annonces actives. Limite : 5 000 appels/jour en sandbox, jusqu'à ~1,5 million d'appels/jour en production selon le plan.
  • Browse API — accès aux détails d'une annonce individuelle (item specifics, prix, livraison). Même plafond global que le Finding API dans le quota partagé.
  • Trading API — opérations de compte (vente, gestion d'annonces). Moins pertinent pour le scraping passif.

Le problème ? Le quota de production n'est pas garanti — il dépend de votre API App ID et de l'historique d'utilisation. Et surtout, les API ne retournent pas tout ce que le HTML affiche : les données de feedback vendeur détaillées, les filtres de recherche visuels, les variantes de prix dynamiques sont souvent absentes ou tronquées.

Les API eBay sont idéales pour des requêtes ciblées et légères. Dès que vous devez monitorer des milliers de catégories en parallèle ou capturer des données que l'API omet, le scraping HTML devient la seule option viable.

Quand scraper plutôt qu'appeler l'API

Voici une comparaison directe pour vous aider à trancher :

CritèreAPI eBay (Finding/Browse)Scraping HTML
Quota quotidienVariable, souvent plafonné à ~1,5M/jourLimité par votre infrastructure proxy
Couverture des donnéesChamps structurés, pas de DOMTout ce qui est rendu dans la page
Données d'enchères en temps réelPartielles via GetItemStatusComplètes (compteur de bids, timer)
Feedback vendeur détailléScore global uniquementGraphe complet, catégories, commentaires
Geo-spécificité (eBay.de, eBay.co.uk)Paramètre siteid disponibleIP locale + domaine régional
Risque de blocageQuota exceeded → HTTP 429IP bans, CAPTCHAs, JS challenges
Coût d'infrastructureGratuit jusqu'au quotaCoût des proxies résidentiels

En pratique, les équipes de market research et de renseignement concurrentiel combinent les deux : l'API pour les lookups ponctuels, le scraping pour les crawls massifs et les données que l'API ne fournit pas.

Structure HTML d'eBay : les sélecteurs qui comptent

eBay utilise un rendu côté serveur pour les pages de résultats — bonne nouvelle, pas besoin de headless browser pour la plupart des cas. Voici les structures clés à cibler.

Page de résultats de recherche

L'URL canonique d'une recherche :

https://www.ebay.com/sch/i.html?_nkw=keyword+here&_sop=12&_pgn=1

Paramètres utiles :

  • _nkw — terme de recherche
  • _sop — tri (12 = fin prochaine, 1 = meilleur match, 15 = prix + livraison croissant)
  • _pgn — numéro de page
  • _ipg — résultats par page (240 max)
  • LH_Sold=1 — inclure les objets vendus
  • LH_Auction=1 — enchères uniquement
  • LH_BIN=1 — Buy-It-Now uniquement

Chaque annonce est encapsulée dans un <li class="s-item">. Les sous-sélecteurs critiques :

  • Titre : .s-item__title ou h3.s-item__title > a
  • Prix : .s-item__price
  • URL de l'annonce : .s-item__link (attribut href)
  • Livraison : .s-item__shipping
  • Localisation : .s-item__location
  • Badge BIN : .s-item__formatBanner contient « Buy It Now»
  • Compteur d'enchères : .s-item__bidCount
  • Timer : .s-item__time-left
  • Image : .s-item__image-wrapper > img (attribut src)

Page de détail d'une annonce

URL typique : https://www.ebay.com/itm/1234567890

Sélecteurs importants :

  • Item specifics : #viTabs_0_is — tableau de caractéristiques
  • Prix actuel : [itemprop='price']
  • Historique des enchères : #BidHistory (visible si connecté)
  • Info vendeur : .x-sellercard ou #seller-info
  • Description : via iframe #desc_ifr — attention, c'est du contenu cross-origin

Page de profil vendeur

URL : https://www.ebay.com/usr/SELLER_NAME

  • Score de feedback : .x-feedback-score
  • Pourcentage positif : .x-feedback-pct
  • Catégories actives : .x-seller-items__category
  • Annones actives : .x-seller-items__count

Technologie anti-bot d'eBay

eBay utilise plusieurs couches de protection :

  • Rate limiting par IP — environ 100-200 requêtes/minute depuis une même IP avant déclenchement d'un CAPTCHA ou d'un HTTP 429.
  • Fingerprinting navigateur — headers TLS (JA3), User-Agent, Accept-Language doivent être cohérents.
  • JavaScript challenges — principalement sur les pages de détail et les pages vendeur. Les résultats de recherche restent principalement SSR.
  • AKAMAI Bot Manager — détecte les patterns de crawling par signature comportementale.

C'est pourquoi les proxies datacenter sont quasi inutilisables sur eBay à grande échelle : les plages d'IP datacenter sont pré-identifiées par Akamai. Les proxies résidentiels sont essentiels.

Choisir le bon proxy pour eBay

Proxies résidentiels : le choix par défaut

eBay bloque les IPs datacenter de manière agressive. Pour tout scrape dépassant ~50 pages, les proxies résidentiels sont obligatoires. Ils simulent du trafic d'utilisateurs réels et passent les filtres d'Akamai.

Geo-ciblage pour les marketplaces régionaux

eBay adapte ses résultats en fonction de votre localisation IP. Pour eBay.de, eBay.co.uk ou eBay.fr, vous devez utiliser des IPs locales :

  • eBay.de → proxy en Allemagne (country-DE)
  • eBay.co.uk → proxy au Royaume-Uni (country-GB)
  • eBay.fr → proxy en France (country-FR)
  • eBay.com.au → proxy en Australie (country-AU)

Avec ProxyHat, le geo-ciblage se configure directement dans le nom d'utilisateur :

# Proxy résidentiel US (par défaut)
http://user-country-US:pass@gate.proxyhat.com:8080

# Proxy résidentiel pour eBay.de
http://user-country-DE:pass@gate.proxyhat.com:8080

# Proxy résidentiel pour eBay.co.uk
http://user-country-GB:pass@gate.proxyhat.com:8080

# Proxy avec ville ciblée (Berlin)
http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080

Sessions sticky vs rotation par requête

  • Rotation par requête — idéale pour les pages de résultats de recherche (chaque page = nouvelle IP). Limite le risque de blocage par IP.
  • Sessions sticky — nécessaires pour les pages de détail où eBay vérifie la cohérence de session (cookies, panier). Utilisez le flag session- :
# Session sticky de 10-30 min
http://user-session-abc123:pass@gate.proxyhat.com:8080

Exemple Python : scraper les résultats de recherche eBay

Voici un eBay listing scraper complet en Python, utilisant des proxies résidentiels ProxyHat et BeautifulSoup :

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass, asdict
import json, time, random

@dataclass
class EbayListing:
    title: str
    price: str
    shipping: str
    url: str
    item_id: str
    is_bin: bool
    bid_count: str | None
    time_left: str | None
    location: str | None

PROXY = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY, "https": PROXY}
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-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml",
}

def build_search_url(keyword: str, page: int = 1, sort: str = "12") -> str:
    return (
        f"https://www.ebay.com/sch/i.html?"
        f"_nkw={requests.utils.quote(keyword)}"
        f"&_sop={sort}&_pgn={page}&_ipg=240"
    )

def parse_listing(li) -> EbayListing | None:
    try:
        title_el = li.select_one(".s-item__title")
        if not title_el or "Shop on eBay" in title_el.get_text():
            return None
        link = li.select_one(".s-item__link")
        url = link["href"].split("?")[0] if link else ""
        item_id = url.rstrip("/").split("/")[-1] if url else ""
        price = li.select_one(".s-item__price").get_text(strip=True) if li.select_one(".s-item__price") else ""
        shipping = li.select_one(".s-item__shipping").get_text(strip=True) if li.select_one(".s-item__shipping") else ""
        is_bin = bool(li.select_one(".s-item__formatBanner"))
        bid_el = li.select_one(".s-item__bidCount")
        bid_count = bid_el.get_text(strip=True) if bid_el else None
        time_el = li.select_one(".s-item__time-left")
        time_left = time_el.get_text(strip=True) if time_el else None
        loc_el = li.select_one(".s-item__location")
        location = loc_el.get_text(strip=True) if loc_el else None
        return EbayListing(
            title=title_el.get_text(strip=True),
            price=price, shipping=shipping, url=url,
            item_id=item_id, is_bin=is_bin,
            bid_count=bid_count, time_left=time_left,
            location=location,
        )
    except Exception as e:
        print(f"  [!] Parse error: {e}")
        return None

def scrape_ebay_search(keyword: str, max_pages: int = 5) -> list[EbayListing]:
    results = []
    for page in range(1, max_pages + 1):
        url = build_search_url(keyword, page)
        print(f"[*] Page {page}: {url}")
        resp = requests.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
        if resp.status_code != 200:
            print(f"  [!] HTTP {resp.status_code} — arrêt")
            break
        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select("li.s-item")
        print(f"  → {len(items)} éléments trouvés")
        for li in items:
            listing = parse_listing(li)
            if listing:
                results.append(listing)
        # Délai aléatoire pour simuler un humain
        time.sleep(random.uniform(2, 5))
    return results

if __name__ == "__main__":
    listings = scrape_ebay_search("vintage rolex", max_pages=3)
    for l in listings[:10]:
        print(json.dumps(asdict(l), indent=2))
    print(f"\nTotal: {len(listings)} annonces extraites")

Sortie exemple (tronquée) :

{
  "title": "Vintage Rolex Submariner 1680",
  "price": "$4,850.00",
  "shipping": "+$25.00 shipping",
  "url": "https://www.ebay.com/itm/3345678901",
  "item_id": "3345678901",
  "is_bin": true,
  "bid_count": null,
  "time_left": null,
  "location": "New York,United States"
}

Gérer les enchères : bids, timers et drapeaux BIN

Les enchères eBay nécessitent une attention particulière car les données changent en temps réel. Voici les stratégies selon votre objectif :

Monitoring en temps réel des enchères

Les pages de résultats affichent un compteur de bids (.s-item__bidCount) et un timer (.s-item__time-left) pour les annonces aux enchères. Pour un suivi rapproché :

  • Rotation rapide — interrogez la même recherche toutes les 60-90 secondes avec une nouvelle IP résidentielle à chaque cycle.
  • Sticky session par annonce — pour les pages de détail, gardez la même IP pendant 10-15 minutes pour maintenir les cookies de session.
  • Détection de la fin d'enchère — quand .s-item__time-left passe en dessous de 5 minutes, augmentez la fréquence de polling à 15-30 secondes.

Extraire les données d'enchères depuis la page de détail

def parse_auction_detail(soup) -> dict:
    """Extrait les données d'enchère depuis une page de détail eBay."""
    result = {
        "current_price": None,
        "bid_count": None,
        "time_left_seconds": None,
        "is_bin": False,
        "bin_price": None,
        "bid_history_url": None,
    }
    # Prix actuel
    price_el = soup.select_one("[itemprop='price']")
    if price_el:
        result["current_price"] = price_el.get("content") or price_el.get_text(strip=True)
    # Compteur de bids
    bid_count_el = soup.select_one("#vi-remaining-bid-count")
    if bid_count_el:
        result["bid_count"] = bid_count_el.get_text(strip=True)
    # Timer (valeur data-attribute si disponible)
    timer_el = soup.select_one("#vi-time-left")
    if timer_el and timer_el.get("data-timestamp"):
        result["time_left_seconds"] = int(timer_el["data-timestamp"]) // 1000
    # Drapeau Buy-It-Now
    bin_el = soup.select_one("#binBtn_btn")
    if bin_el:
        result["is_bin"] = True
        bin_price_el = soup.select_one("#prcIsum-bin")
        if bin_price_el:
            result["bin_price"] = bin_price_el.get_text(strip=True)
    # Lien historique des enchères
    hist_el = soup.select_one("a[href*='bidhistory']")
    if hist_el:
        result["bid_history_url"] = hist_el["href"]
    return result

Pattern de sniping : ne pas abuser

Le scraping d'enchères en temps réel peut être utilisé pour du sniping (enchère à la dernière seconde). Attention : eBay détecte les patterns de sniping automatisé et peut suspendre les comptes. Utilisez ces données pour de l'analyse de marché, pas pour de la manipulation d'enchères.

Analytique vendeur : feedback, catégories et cross-listing

Le profil vendeur est une mine d'or pour le renseignement concurrentiel. Voici comment extraire et structurer ces données.

Scraping du profil vendeur

URL : https://www.ebay.com/usr/{seller_name}

Cette page contient :

  • Score de feedback — nombre total de transactions évaluées
  • Pourcentage positif — fiabilité perçue
  • Catégories actives — dans quelles catégories le vendeur liste ses produits
  • Nombre d'annonces actives — volume de stock actuel
  • Date d'inscription — ancienneté du compte
def scrape_seller_profile(seller_name: str) -> dict:
    url = f"https://www.ebay.com/usr/{seller_name}"
    resp = requests.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
    soup = BeautifulSoup(resp.text, "html.parser")
    
    profile = {
        "seller": seller_name,
        "feedback_score": None,
        "positive_pct": None,
        "active_listings_count": None,
        "categories": [],
        "member_since": None,
    }
    # Score de feedback
    score_el = soup.select_one(".x-feedback-score")
    if score_el:
        profile["feedback_score"] = score_el.get_text(strip=True)
    # Pourcentage positif
    pct_el = soup.select_one(".x-feedback-pct")
    if pct_el:
        profile["positive_pct"] = pct_el.get_text(strip=True)
    # Catégories actives
    cat_els = soup.select(".x-seller-items__category")
    profile["categories"] = [c.get_text(strip=True) for c in cat_els]
    # Nombre d'annonces actives
    count_el = soup.select_one(".x-seller-items__count")
    if count_el:
        profile["active_listings_count"] = count_el.get_text(strip=True)
    # Date d'inscription
    since_el = soup.select_one(".x-member-since")
    if since_el:
        profile["member_since"] = since_el.get_text(strip=True)
    return profile

Détection de cross-listing

Les revendeurs listent souvent les mêmes produits sur plusieurs marchés eBay. Pour détecter les patterns de cross-listing :

  1. Scrapez les résultats de recherche pour un mot-clé sur ebay.com, ebay.de et ebay.co.uk simultanément en utilisant des proxies geo-ciblés.
  2. Comparez les titres normalisés (lowercase, suppression des espaces) et les images (hash perceptuel).
  3. Croisez les noms de vendeurs — un même vendeur sur plusieurs domaines eBay est un signal fort de cross-listing.

Exemple de requêtes parallèles avec geo-ciblage :

import concurrent.futures

DOMAINS = {
    "US": "https://www.ebay.com",
    "DE": "https://www.ebay.de",
    "GB": "https://www.ebay.co.uk",
}

def get_geo_proxy(country: str) -> dict:
    proxy = f"http://user-country-{country}:PASSWORD@gate.proxyhat.com:8080"
    return {"http": proxy, "https": proxy}

def scrape_regional(keyword: str, country: str) -> list:
    base = DOMAINS[country]
    url = f"{base}/sch/i.html?_nkw={keyword}&_ipg=240"
    proxies = get_geo_proxy(country)
    resp = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
    soup = BeautifulSoup(resp.text, "html.parser")
    return [parse_listing(li) for li in soup.select("li.s-item") if parse_listing(li)]

def cross_market_scan(keyword: str) -> dict:
    results = {}
    with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
        futures = {
            pool.submit(scrape_regional, keyword, c): c
            for c in DOMAINS
        }
        for f in concurrent.futures.as_completed(futures):
            country = futures[f]
            results[country] = f.result()
    return results

Bonnes pratiques et considérations éthiques

  • Respectez robots.txt — eBay autorise le crawling partiel mais restreint certains chemins. Vérifiez https://www.ebay.com/robots.txt régulièrement.
  • Limitez votre taux de requêtes — 1-2 requêtes/seconde par IP est un maximum raisonnable. Les rafales sont immédiatement détectées.
  • Conformité GDPR/CCPA — les données vendeur (nom, localisation, historique) sont des données personnelles en Europe. Stockez-les de manière conforme.
  • Données de prix — les prix extraits ne doivent pas être utilisés pour de la manipulation de marché ou du contournement de la concurrence.
  • Termes de service — le scraping massif viole généralement les ToS d'eBay. Utilisez les API officielles autant que possible et limitez le scraping aux cas où l'API ne suffit pas.

Points clés à retenir

  • Les API eBay (Finding/Browse) couvrent les besoins basiques mais imposent des quotas et omettent des données critiques (feedback détaillé, enchères temps réel, cross-listing).
  • Le sélecteur li.s-item est la clé de voûte du parsing des résultats de recherche eBay — tous les champs importants en dérivent.
  • Les proxies datacenter sont inutilisables sur eBay à cause d'Akamai — les proxies résidentiels sont obligatoires pour tout scrape au-delà de ~50 pages.
  • Le geo-ciblage est essentiel pour les marketplaces régionaux (eBay.de, eBay.co.uk, eBay.fr) — configurez-le via le nom d'utilisateur ProxyHat.
  • Les enchères nécessitent un polling fréquent avec rotation d'IP et des sessions sticky pour les pages de détail.
  • L'analytique vendeur (feedback, catégories, cross-listing) est la donnée la plus stratégique — et la moins accessible via l'API.

Prêt à scraper eBay à grande échelle ? Consultez notre page de tarification pour les plans de proxies résidentiels, ou explorez nos localisations disponibles pour le geo-ciblage. Pour d'autres guides techniques, lisez notre article sur le scraping web avec proxies résidentiels.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog