How to scrape ebay with python cover 1 2

This guide shows you how to scrape eBay with Python end to end: a working scraper for search results, individual listings, product variants, and reviews — plus the anti-bot and proxy setup that keeps it running. You’ll need Python 3, a few libraries, and about 30 minutes for the basic version. By the end you’ll have reusable eBay web scraping functions that export clean JSON and CSV.

I’m Andrii Byzov, an AI-Native Fractional CMO who runs e-commerce data pipelines. eBay is a high-value but well-defended target, so this walkthrough on how to scrape eBay pairs the code with the parts most guides skip — pagination, hidden variant data, and avoiding blocks. The short version of scraping eBay safely: send real browser headers, rotate residential proxies, and respect rate limits. For which IPs to use, see our best proxies for eBay scraping guide.


Key Facts

  • eBay has two page types — search results and product listings — with different HTML structures, so you write two scrapers.
  • Variants (size, colour, storage) are hidden in inline JSON, not visible HTML — you extract them with a regex and json.loads().
  • Real browser headers come first — without a proper User-Agent eBay often returns 403 or empty pages even for simple requests.
  • Datacenter IPs get flagged fast on eBay; rotating residential proxies sustain volume and return region-correct prices.
  • Listings differ by country — eBay.com, eBay.co.uk, and eBay.de show different prices and shipping, so geotargeting matters for accurate eBay data scraping.

Step 1 — Set Up Your Environment

Create an isolated environment and install the libraries your eBay web scraping setup needs. Each one has a job — requests fetches pages, beautifulsoup4 + lxml parse the HTML, and httpx handles async requests once you scale.



# Create an isolated project folder and virtual environment
mkdir ebay-scraper && cd ebay-scraper
python3 -m venv env
source env/bin/activate        # Windows: env\Scripts\activate

# requests  -> send HTTP requests
# beautifulsoup4 -> parse the returned HTML
# lxml -> fast HTML parser backend for BeautifulSoup
# httpx -> async requests for scaling later
pip install requests beautifulsoup4 lxml httpx










Step 2 — Understand eBay’s Page Structure

Before writing code, understand that eBay has two fundamentally different page types — and how to scrape eBay reliably starts with knowing which selectors belong to each. Scraping each page type needs different selectors. Open any page, press F12 (or right-click → Inspect), and hover over an element to find its class. The map below is what you’ll reference in Steps 3 and 4.

Search Results Page

Each result sits in a li.s-item container, with the title, price, and link nested inside. The _pgn URL parameter controls pagination — you’ll need it in Step 3.

Product Listing Page

A single listing exposes the price in .x-price-primary, the title in h1 span, and item specifics in .ux-labels-values rows. Note that the long description sits in a separate iframe#desc_ifr — a sub-page you’d scrape with its own request.

Data Search results selector Listing page selector
Container li.s-item
Title .s-item__title h1 span
Price .s-item__price .x-price-primary
Link / image a.s-item__link / .s-item__image img
Item specifics .ux-labels-values
Description iframe#desc_ifr

Step 3 — Scrape eBay Search Results

The first scraper takes a search query and returns a list of items with title, price, URL, and image — the entry point for most eBay web scraping projects. Without the right User-Agent header eBay returns a 403 or an empty page even for this simple request, so the headers go in from the start. You can adapt the same pattern to scrape eBay listings from any search term.



import requests
from bs4 import BeautifulSoup
from urllib.parse import quote_plus

HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                         "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
           "Accept-Language": "en-US,en;q=0.9"}

def scrape_search(query, proxies=None):
    """Return a list of eBay search results for a query."""
    url = f"https://www.ebay.com/sch/i.html?_nkw={quote_plus(query)}"
    html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
    soup = BeautifulSoup(html.text, "lxml")
    items = []
    for card in soup.select("li.s-item"):
        title = card.select_one(".s-item__title")
        price = card.select_one(".s-item__price")
        link  = card.select_one("a.s-item__link")
        image = card.select_one(".s-item__image-wrapper img")
        if not (title and link):
            continue
        items.append({
            "title": title.get_text(strip=True),
            "price": price.get_text(strip=True) if price else None,
            "url":   link["href"],
            "image": image.get("src") if image else None,
        })
    return items

print(scrape_search("smartphone")[:3])






























Handling Pagination

One result page isn’t enough. This loop walks the _pgn parameter (page 2, 3 … N) and stops when the result list is empty or you hit a page limit — the piece most eBay scraper Python tutorials leave out.



from urllib.parse import quote_plus

def scrape_all_pages(query, max_pages=10, proxies=None):
    """Page through results via the _pgn parameter until empty or max_pages."""
    all_items = []
    for page in range(1, max_pages + 1):
        url = (f"https://www.ebay.com/sch/i.html?_nkw={quote_plus(query)}"
               f"&_pgn={page}")
        html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
        soup = BeautifulSoup(html.text, "lxml")
        cards = soup.select("li.s-item")
        if not cards:                 # no more results -> stop
            break
        for card in cards:
            title = card.select_one(".s-item__title")
            link  = card.select_one("a.s-item__link")
            if title and link:
                all_items.append({"title": title.get_text(strip=True),
                                  "url": link["href"]})
    return all_items




















Step 4 — Scrape Product Listing Pages

When you scrape eBay listings one at a time, parse_product(url) takes a single listing URL and returns the structured fields: title, price, condition, seller, shipping, and the item-specifics table where each row is a key/value pair. This is the core of eBay data scraping at the product level.



def parse_product(url, proxies=None):
    """Scrape a single eBay listing page."""
    html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
    soup = BeautifulSoup(html.text, "lxml")

    def text(sel):
        el = soup.select_one(sel)
        return el.get_text(strip=True) if el else None

    # item specifics live in a label/value table
    specifics = {}
    for row in soup.select(".ux-labels-values"):
        label = row.select_one(".ux-labels-values__labels")
        value = row.select_one(".ux-labels-values__values")
        if label and value:
            specifics[label.get_text(strip=True)] = value.get_text(strip=True)

    return {
        "title":     text("h1 span"),
        "price":     text(".x-price-primary"),
        "condition": text(".x-item-condition-text .ux-textspans"),
        "seller":    text(".x-sellercard-atf__info__about-seller"),
        "shipping":  text(".ux-labels-values--shipping .ux-textspans"),
        "item_specifics": specifics,
    }

























Extracting Product Variants (MSKU Data)

eBay keeps product variant options — colour, size, storage — not in visible HTML but inside an inline JSON object (MSKU) within a <script> tag. You locate that JSON and parse it with json.loads(). Its exact key and shape change over time, so treat the code below as a starting point and adjust it to what you see in DevTools — it returns the variant options (name/value), the part most competitors skip.



import re, json

def parse_variants(url, proxies=None):
    """eBay keeps multi-SKU variant options (size, colour, storage) in an inline
    JSON blob rather than visible HTML. The exact key and shape change over time,
    so treat the pattern below as a starting point and adjust it to the page you
    inspect in DevTools. This returns the option name/value pairs, not full
    price-per-combination data."""
    html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30).text
    # find the inline state object, then load it with a balanced-brace slice
    start = html.find('"MSKU"')
    if start == -1:
        return []
    brace = html.find("{", start)
    depth, end = 0, brace
    for i in range(brace, len(html)):
        depth += (html[i] == "{") - (html[i] == "}")
        if depth == 0:
            end = i + 1
            break
    try:
        data = json.loads(html[brace:end])
    except json.JSONDecodeError:
        return []
    return [{"name": v.get("displayName"), "value": v.get("valueName")}
            for v in data.get("menuItemMap", {}).values()]


























Step 5 — Scrape eBay Product Reviews

When scraping eBay reviews, note they’re product/catalog reviews on a separate URL, not on every listing page — and not every listing has them. You derive the review URL from the product/catalog ID found on the listing, then pull rating, text, date, and author. Treat the endpoint below as illustrative and confirm the current path in DevTools.



def scrape_reviews(item_id, proxies=None):
    """Reviews live on a separate URL keyed by the item ID."""
    url = f"https://www.ebay.com/urw/{item_id}/product-reviews"
    html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)
    soup = BeautifulSoup(html.text, "lxml")
    reviews = []
    for r in soup.select(".ebay-review-section"):
        rating = r.select_one(".star-rating")
        body   = r.select_one(".review-item-content")
        date   = r.select_one(".review-item-date")
        author = r.select_one(".review-item-author")
        reviews.append({
            "rating": rating.get("aria-label") if rating else None,
            "text":   body.get_text(strip=True) if body else None,
            "date":   date.get_text(strip=True) if date else None,
            "author": author.get_text(strip=True) if author else None,
        })
    return reviews


















Step 6 — Export Data to JSON and CSV

Save your results in both formats side by side: JSON keeps the nested structure (useful for variants and specifics), CSV is flat and opens straight in Excel or Google Sheets.



import json, csv

results = scrape_search("smartphone")

# Option A - JSON (keeps nested structure)
with open("ebay.json", "w", encoding="utf-8") as f:
    json.dump(results, f, indent=2, ensure_ascii=False)

# Option B - CSV (flat, opens in Excel/Sheets)
if results:
    with open("ebay.csv", "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=results[0].keys())
        writer.writeheader()
        writer.writerows(results)














Step 7 — Bypass eBay’s Anti-Bot Measures

eBay defends listings with a layered stack: IP-reputation scoring, rate limiting (HTTP 429), soft blocks (a 200 response with an empty result), and CAPTCHAs. You won’t defeat all of it with one trick, but the right headers and IPs handle the common cases for eBay web scraping at a reasonable scale.

Set Proper Headers

Send a full, realistic header set — not just a User-Agent. Each header lowers your block risk: a missing Accept-Language or Referer is a common bot tell. For larger jobs, rotate the User-Agent from a list.



HEADERS = {
    # identify as a real desktop browser - eBay returns 403/empty without this
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",      # match the locale you target
    "Accept-Encoding": "gzip, deflate, br",   # accept compressed responses
    "Referer": "https://www.ebay.com/",       # look like in-site navigation
    "Connection": "keep-alive",
}

# rotate the User-Agent across a list for larger jobs
import random
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15",
]
HEADERS["User-Agent"] = random.choice(USER_AGENTS)

















Use Rotating Residential Proxies

Headers aren’t enough at volume — eBay scores IP reputation, and datacenter proxies get flagged far faster than residential ones. Use rotating residential IPs for high-volume search scraping (a fresh IP per request) and sticky sessions for listing or auction flows where a stable session matters. DataImpulse residential proxies are $1/GB, pay-as-you-go, and plug straight into the requests call.



# Route requests through DataImpulse rotating residential proxies
proxies = {
    "http":  "http://LOGIN:[email protected]:823",
    "https": "http://LOGIN:[email protected]:823",
}
html = requests.get(url, headers=HEADERS, proxies=proxies, timeout=30)






Geotargeting — Scraping Region-Specific eBay Data

eBay listings differ by country: eBay.com, eBay.co.uk, and eBay.de return different prices, availability, and shipping for the same product. To see the data a buyer in a specific country sees, route through a proxy located there. DataImpulse has residential IPs across 195 countries, including US state-level targeting, so your eBay data scraping reflects the right market. The same approach powers price comparison across regions.


Step 8 — Scale Your eBay Scraper

Three levels turn the basic script into a production-ready way to scrape eBay with Python at scale, in order of increasing complexity: a random delay between requests to stay under rate limits, retry logic with exponential backoff to absorb 429/403 responses, and async collection with httpx + asyncio for parallel volume.



import time, random, asyncio, httpx

# 1) Basic pacing - a random delay between requests
def polite_get(url, **kw):
    time.sleep(random.uniform(1, 3))
    return requests.get(url, headers=HEADERS, timeout=30, **kw)

# 2) Retry with exponential backoff on 429/403
def get_with_retry(url, retries=4, **kw):
    for attempt in range(retries):
        r = requests.get(url, headers=HEADERS, timeout=30, **kw)
        if r.status_code not in (429, 403):
            return r
        time.sleep(2 ** attempt + random.random())   # 1s, 2s, 4s, 8s ...
    return r

# 3) Async collection for high volume (httpx >=0.28 uses proxy=, not proxies=)
async def scrape_many(urls, proxy=None):
    async with httpx.AsyncClient(headers=HEADERS, proxy=proxy, timeout=30) as client:
        tasks = [client.get(u) for u in urls]
        return await asyncio.gather(*tasks)





















Frequently Asked Questions

Is it legal to scrape eBay?

It depends. Scraping publicly available data can be lawful in some contexts, but eBay’s User Agreement restricts automated access — bots, scrapers, and data-mining tools — without prior permission, so scraping can breach their terms and carry legal risk. Using proxies itself is legal. Respect rate limits, don’t scrape behind logins, avoid personal data, and get legal advice for commercial use. This is general information, not legal advice — see our guide on whether web scraping is legal.

Do I need proxies to scrape eBay?

For a handful of requests, no. For any real volume, yes — eBay scores IP reputation and rate-limits hard, so a single IP gets blocked quickly. Rotating residential proxies spread requests across many IPs and let you target specific countries for region-correct prices.

How do I scrape eBay product variants?

Variants like size and colour aren’t in the visible HTML — eBay stores them in an inline JSON (MSKU) object inside a script tag. Locate that object with re.search() and parse it with json.loads(), as shown in Step 4.

Why is my eBay scraper returning empty results?

Most often it’s headers. Without a real browser User-Agent (and ideally Accept-Language and Referer), eBay frequently returns a 403 or a 200 with an empty body. Add the full header set from Step 7; if it persists, you’re likely rate-limited or IP-flagged, so slow down and route through a residential proxy.

How do I handle eBay pagination in Python?

Increment the _pgn URL parameter (&_pgn=2, 3, …) in a loop and stop when the result container (li.s-item) is empty or you reach a page cap, as in the pagination function in Step 3.


Conclusion

That’s how to scrape eBay with Python end to end: search-results and listing scrapers, pagination, hidden-variant extraction, reviews, JSON/CSV export, and the headers plus rotating residential proxies that keep your eBay scraper Python setup running. Start small with the search scraper, then layer in pagination, anti-bot headers, and scaling as your volume grows. For the right IPs, see best proxies for eBay scraping.

Last updated: June 24, 2026.



Share article: