Cómo scrapear eBay con proxies: guía pragmática para equipos de reventa e inteligencia de mercado

Aprende a scrapear eBay de forma fiable: cuándo usar las APIs oficiales vs. scraping HTML, selectores concretos, rotación de IPs residenciales con ProxyHat y código Python listo para ejecutar.

Cómo scrapear eBay con proxies: guía pragmática para equipos de reventa e inteligencia de mercado

APIs de eBay vs. scraping HTML: la disyuntiva real

Si necesitas datos de eBay a escala, lo primero que debes decidir es si usas las APIs oficiales o raspas el HTML directamente. Las eBay Finding API y la Browse API parecen la opción limpia, pero tienen costes ocultos que te empujarán al scraping antes de lo que esperas.

Cuotas y límites de las APIs de eBay

  • Finding API: 5 000 llamadas/día en el tier básico; sube a 10 000 si solicitas aumento. Cada llamada devuelve hasta 100 ítems (paginación de 100 páginas máximo = 10 000 ítems por consulta).
  • Browse API: depende del sandbox/producción y del acuerdo comercial. En producción típica verás ~10 000 llamadas/día, con un rate limit de ~500 llamadas/min.
  • Trading API: orientada a vendedores, no a lectura masiva. 15 000 llamadas/día, pero requiere tokens de usuario con permisos específicos.
  • OAuth token refresh: los tokens de usuario expiran cada 18 horas; los de aplicación duran 2 años. Gestionar el refresh a escala es un punto de fallo real.

El problema no es solo la cuota: la Finding API no expone datos de subastas en tiempo real (bid count, time-to-end preciso) ni la estructura completa del feedback del vendedor. Si tu caso de uso es inteligencia de precios para reventa, esos campos son exactamente los que necesitas.

Cuándo recurrir al scraping

  • Necesitas datos de subastas en vivo (bid count, tiempo restante).
  • Quieres analizar perfiles de vendedor: feedback detallado, categorías de listing, patrones de cross-listing.
  • Tu volumen supera los 10 000 ítems/día y el aumento de cuota tarda semanas.
  • Necesitas datos de eBay regionales (eBay.de, eBay.co.uk) que la API no filtra con granularidad suficiente.

Regla práctica: usa la API para prototipos rápidos y volúmenes bajos. Migrar a scraping HTML cuando necesites profundidad de datos o escala.

Estructuras HTML objetivo en eBay

Conocer los selectores correctos te ahorra horas de depuración. Estos son los tres bloques principales que raspamos en eBay.

Grid de resultados de búsqueda: .s-item

La página de resultados (/sch/i.html?_nkw=KEYWORD) renderiza cada listing como un <li class="s-item">. Los campos clave:

DatoSelector CSSNotas
Título.s-item__titleTexto limpio; evita .s-item__title--tag (badges como "NEW")
Precio.s-item__pricePuede incluir rango "$10.00 to $15.00"
Envío.s-item__shipping"Free shipping", "$5.99 shipping"
URL del listing.s-item__linkhrefIncluye tracking params; límpialos
Imagen.s-item__image-imgsrcThumbnail 225×225; reemplaza s-l225 por s-l1600 para HD
Subasta / BIN.s-item__bid-count, .s-item__buy-it-nowVacío si no aplica
Tiempo restante.s-item__time-left"1h 5m left" — formato humano
Ubicación.s-item__location"from United States"

Página de detalle del listing

La URL canónica es /itm/ITEM_ID. Selectores útiles:

  • Precio actual / bid: #prcIsum o #mm-saleDscPrc
  • Buy-It-Now price: #binBtn_btn → atributo o texto adyacente
  • Bid count: #bidCount o .vi-bid-count
  • Tiempo restante de subasta: #vi-cdown_timeLeft — formato "Xh Xm left"
  • Item specifics: .ux-labels-values__labels y .ux-labels-values__values — pares clave-valor
  • Descripción: dentro de un iframe #desc_ifr — necesitas una segunda request al src del iframe

Perfil del vendedor

URL: /usr/SELLER_NAME o /str/STORE_NAME. Campos:

  • Feedback score: .mbg-n (número entre paréntesis)
  • Positive feedback %: #si-fb
  • Feedback detallado: en /usr/SELLER_NAME/feedback, tabla con .fb-table
  • Categorías de la tienda: .str-header__categories → enlaces con nombres

Anti-bot de eBay: lo que te va a bloquear

eBay usa una combinación de técnicas:

  • Fingerprinting de TLS/JA3: los datacenter IPs con huellas TLS de librerías de scraping (Python requests con defaults) se bloquean rápido.
  • Rate limiting por IP: ~200 requests/min desde una IP residencial antes de CAPTCHA; ~50 requests/min desde DC.
  • ReCAPTCHA v3: aparece en páginas de búsqueda tras ~10 páginas seguidas desde la misma IP.
  • Device fingerprint (JavaScript): si no ejecutas JS, eBay puede servir una página reducida o devolver HTTP 202 con challenge.

Por eso los proxies residenciales son prácticamente obligatorios para scraping de eBay a escala.

Selección de proxy para eBay

Residencial vs. Datacenter vs. Móvil

TipoBloqueo en eBayCaso de uso
DatacenterAlto — bloqueado tras ~50 req/minPrototipado, APIs oficiales
ResidencialBajo — ~200 req/min por IPScraping masivo de listings y búsqueda
MóvilMuy bajo — eBay confía en IPs móvilesAlta frecuencia en subastas en vivo

Geo-targeting para eBay regionales

Los precios y la disponibilidad cambian según el país. eBay.de muestra precios en EUR y listings exclusivos de Alemania; eBay.co.uk tiene su propio inventario. Con ProxyHat, geo-targeting se controla desde el nombre de usuario:

# eBay.de — IPs alemanas
http://user-country-DE:PASSWORD@gate.proxyhat.com:8080

# eBay.co.uk — IPs británicas
http://user-country-GB:PASSWORD@gate.proxyhat.com:8080

# eBay.com.au — IPs australianas
http://user-country-AU:PASSWORD@gate.proxyhat.com:8080

Rotación por-request vs. sesiones sticky

  • Rotación por-request: cada request obtiene una IP diferente. Ideal para recorrer páginas de resultados sin disparar rate limits. Simplemente no pongas flag de sesión.
  • Sticky session: mantiene la misma IP durante un tiempo (típicamente 10–30 min). Necesario cuando eBay requiere consistencia de sesión (ej. navegar página 1 → 2 → 3 sin que te resetee el carrito o filtros).
# Rotación por-request (sin flag de sesión)
http://user-country-US:PASSWORD@gate.proxyhat.com:8080

# Sticky session para paginación
http://user-country-US-session-mypagination123:PASSWORD@gate.proxyhat.com:8080

Ejemplo Python: scraper de búsqueda eBay con ProxyHat

Este script busca un keyword, recorre páginas de resultados y extrae los campos clave de cada .s-item.

import requests
from bs4 import BeautifulSoup
import csv
import time
import re

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-Language": "en-US,en;q=0.9",
}

def build_search_url(keyword, page=1):
    """Construye la URL de búsqueda de eBay con paginación."""
    return (
        f"https://www.ebay.com/sch/i.html?"
        f"_nkw={requests.utils.quote(keyword)}"
        f"&_pgn={page}"
    )

def parse_s_item(li):
    """Extrae datos estructurados de un elemento .s-item."""
    def sel(css):
        el = li.select_one(css)
        return el.get_text(strip=True) if el else None

    link_el = li.select_one(".s-item__link")
    url = link_el["href"].split("?")[0] if link_el else None

    img_el = li.select_one(".s-item__image-img")
    image = img_el["src"].replace("s-l225", "s-l1600") if img_el else None

    price_raw = sel(".s-item__price")
    price = None
    if price_raw:
        m = re.search(r"[\d,]+\.?\d*", price_raw.replace(",", ""))
        price = float(m.group()) if m else None

    bid_count = sel(".s-item__bid-count")
    is_auction = bid_count is not None
    is_bin = li.select_one(".s-item__buy-it-now") is not None
    time_left = sel(".s-item__time-left")

    return {
        "title": sel(".s-item__title"),
        "price": price,
        "currency": "USD",  # ajustar según marketplace
        "shipping": sel(".s-item__shipping"),
        "url": url,
        "image": image,
        "is_auction": is_auction,
        "bid_count": bid_count,
        "is_buy_it_now": is_bin,
        "time_left": time_left,
        "location": sel(".s-item__location"),
    }

def scrape_ebay_search(keyword, max_pages=5):
    """Raspa páginas de búsqueda de eBay y devuelve lista de dicts."""
    results = []
    for page in range(1, max_pages + 1):
        url = build_search_url(keyword, page)
        resp = requests.get(url, headers=HEADERS, proxies=PROXIES, timeout=30)
        resp.raise_for_status()

        soup = BeautifulSoup(resp.text, "html.parser")
        items = soup.select(".s-item")

        # El primer .s-item suele ser un placeholder de "Shop on eBay"
        for li in items[1:]:
            record = parse_s_item(li)
            if record["title"]:
                results.append(record)

        print(f"Página {page}: {len(items) - 1} listings")
        time.sleep(2)  # ser respetuoso entre páginas
    return results

if __name__ == "__main__":
    data = scrape_ebay_search("vintage synthesizer", max_pages=3)
    with open("ebay_listings.csv", "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        writer.writerows(data)
    print(f"Guardados {len(data)} listings en ebay_listings.csv")

Manejo de subastas: bid count, tiempo restante y Buy-It-Now

Las subastas son donde el scraping HTML supera claramente a la API. La Finding API devuelve listingInfo.listingType=Auction, pero el bid count en tiempo real y el tiempo restante preciso solo están en la página de detalle.

Snippet: extraer datos de subasta desde la página de detalle

def parse_auction_detail(soup):
    """Extrae datos de subasta desde /itm/ITEM_ID."""
    def sel(css):
        el = soup.select_one(css)
        return el.get_text(strip=True) if el else None

    # Precio actual de bid
    current_bid_el = soup.select_one("#prcIsum")
    current_bid = None
    if current_bid_el:
        m = re.search(r"[\d,]+\.?\d*", current_bid_el.get_text(strip=True).replace(",", ""))
        current_bid = float(m.group()) if m else None

    # Buy-It-Now price (si existe)
    bin_price = None
    bin_btn = soup.select_one("#binBtn_btn")
    if bin_btn:
        bin_text = bin_btn.get_text(strip=True)
        m = re.search(r"[\d,]+\.?\d*", bin_text.replace(",", ""))
        bin_price = float(m.group()) if m else None

    # Bid count
    bid_count = sel("#bidCount") or sel(".vi-bid-count")

    # Tiempo restante
    time_left = sel("#vi-cdown_timeLeft")

    # Item specifics
    specifics = {}
    labels = soup.select(".ux-labels-values__labels .ux-textsp")
    values = soup.select(".ux-labels-values__values .ux-textsp")
    for l, v in zip(labels, values):
        key = l.get_text(strip=True).rstrip(":")
        val = v.get_text(strip=True)
        if key and val:
            specifics[key] = val

    return {
        "current_bid": current_bid,
        "bin_price": bin_price,
        "bid_count": bid_count,
        "time_left": time_left,
        "is_auction": bid_count is not None,
        "is_buy_it_now": bin_price is not None,
        "item_specifics": specifics,
    }

# Uso:
# resp = requests.get(item_url, headers=HEADERS, proxies=PROXIES, timeout=30)
# soup = BeautifulSoup(resp.text, "html.parser")
# auction_data = parse_auction_detail(soup)

Estrategia de polling para subastas

Si monitorizas subastas cerca del cierre, necesitas polling frecuente. Reglas prácticas:

  • >1 hora restante: poll cada 15 minutos.
  • 15 min – 1 hora: poll cada 2 minutos.
  • <15 min: poll cada 30 segundos (usa IPs móviles para minimizar bloqueo).

Con ProxyHat, cambia a proxies móviles para esta fase:

# Proxy móvil para alta frecuencia
MOBILE_PROXY = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
# (ProxyHat enruta automáticamente a IPs móviles según el plan contratado)

Analítica de vendedores: feedback, categorías y cross-listing

Para equipos de inteligencia de reventa, entender al vendedor es tan importante como el precio del ítem.

Extracción del perfil del vendedor

def parse_seller_profile(soup):
    """Extrae datos del perfil de vendedor desde /usr/SELLER_NAME."""
    def sel(css):
        el = soup.select_one(css)
        return el.get_text(strip=True) if el else None

    # Feedback score (número entre paréntesis)
    score_el = soup.select_one(".mbg-n")
    feedback_score = int(re.search(r"\d+", score_el.get_text()).group()) if score_el else 0

    # Positive feedback %
    positive_pct = sel("#si-fb")

    # Fecha de registro del vendedor
    reg_date = sel("#memberSince")

    # Categorías de la tienda
    categories = []
    cat_links = soup.select(".str-header__categories a")
    for a in cat_links:
        categories.append({
            "name": a.get_text(strip=True),
            "url": a["href"],
        })

    return {
        "feedback_score": feedback_score,
        "positive_feedback_pct": positive_pct,
        "member_since": reg_date,
        "store_categories": categories,
        "category_count": len(categories),
    }

Detección de cross-listing

Un vendedor que lista en múltiples categorías de eBay puede estar diversificando inventario o haciendo retail arbitrage. Para detectar esto:

  1. Raspa el perfil del vendedor y obtén sus categorías de tienda.
  2. Para cada categoría, extrae los listings y compara títulos/precios con los de otras categorías.
  3. Si un vendedor tiene >5 categorías activas con listings recientes, es probable que haga cross-listing o arbitraje.

Feedback detallado como señal de calidad

La página /usr/SELLER_NAME/feedback contiene una tabla con feedback por mes. Extrae:

  • Positive / Neutral / Negative por mes: .fb-table .positive, .fb-table .neutral, .fb-table .negative
  • Comentarios recientes: .fb-table .fb-item-feedback-detail
  • Un vendedor con >2% de feedback negativo en los últimos 3 meses es una señal de alerta para equipos de reventa.

Comando curl rápido para testing

Antes de escribir código, valida que tu proxy funciona con eBay:

# Test rápido: buscar "airpods" en eBay via ProxyHat
curl -x http://user-country-US:PASSWORD@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.ebay.com/sch/i.html?_nkw=airpods&_pgn=1" \
  -o ebay_test.html

# Verificar que hay listings
grep -c 's-item__title' ebay_test.html

Consideraciones éticas y legales

  • robots.txt: eBay permite crawling limitado de ciertas rutas, pero prohíbe scraping de /itm/ y /sch/. Respeta sus términos si operas dentro de su jurisdicción.
  • Rate limiting: nunca superes 1 request/segundo por IP. Usa delays de 2–5 segundos entre páginas.
  • Datos personales: nombres de vendedores y ubicaciones son datos personales bajo GDPR/CCPA. Almacénalos solo si tienes base legal y los anonimizas cuando sea posible.
  • ToS de eBay: el scraping masivo viola los Términos de Servicio. Si eres un negocio registrado, considera usar la API oficial con un acuerdo comercial.

Puntos clave

API vs. Scraping: Usa la Finding API para prototipos y volúmenes <10K ítems/día. Migrar a scraping HTML para datos de subastas, feedback de vendedor y escala superior.

Selectores: .s-item para la grid de búsqueda, #prcIsum y #bidCount para subastas en detalle, .mbg-n para feedback del vendedor.

Proxies residenciales: Obligatorios para scraping de eBay a escala. Datacenter se bloquea tras ~50 req/min; residencial aguanta ~200 req/min por IP.

Geo-targeting: Usa user-country-DE o user-country-GB en ProxyHat para acceder a inventario regional de eBay.de o eBay.co.uk.

Subastas: Aumenta la frecuencia de polling conforme se acerca el cierre. Usa proxies móviles para la fase final (<15 min).

Ética: Respeta rate limits, robots.txt y la privacidad de datos. El scraping masivo viola los ToS de eBay.

Próximos pasos

Si necesitas escalar tu scraping de eBay, configura tus proxies residenciales en ProxyHat y ejecuta el script Python de arriba como punto de partida. Para casos de uso más amplios de web scraping, consulta nuestra guía de web scraping con proxies. Y si rastreas precios de competidores en múltiples marketplaces, el caso de uso de SERP tracking te será útil.

¿Listo para empezar?

Accede a más de 50M de IPs residenciales en más de 148 países con filtrado impulsado por IA.

Ver preciosProxies residenciales
← Volver al Blog