Category: Scraping use cases

2025 Guide to Scraping eBay Listings, Reviews, Search Results, Product Variants

17 mins read Created Date: July 31, 2025   Updated Date: July 31, 2025

Although the recent Temu and Amazon boom slowed down some of the traffic for eBay, they remain to be a solid player for e-commerce scraping with millions of products selling out still.

This guide will help you scrape product listings (with variants), search results & categories, and of course, product reviews from eBay.

We’ll use basic Python libraries like Requests to scrape the website, and level-up our scraper with Scrape.do to bypass any blocks.

Find all working code in this GitHub repository ⚙

Let’s get to it!

To stay ethical and for maximum web scraping legality:

  • Avoid scraping pages behind login
  • Don’t flood servers with high request volumes
  • Don’t resell their content or present it as your own

Scraping Product Data from an eBay Listing (Simple Page)

At its core, every e-commerce site comes down to product pages, and scraping product names and prices the first task for scrapers.

So, we’ll start by scraping a single eBay product page. I’ve been looking for a good mouse for my PC recently, so I’m picking this one.

We want to extract:

  • Product Name
  • Price
  • ePID (eBay Product ID)
  • Product Image URL

Let’s take it step by step.

1. Sending the First Request

We’ll begin by sending a GET request through Scrape.do to ensure we can access the product page without getting blocked.

Don’t forget to grab your Scrape.do API token from here, for free:

import requests
import urllib.parse

# Scrape.do API token
TOKEN = "<your-token>"

# Updated eBay product URL
target_url = "https://www.ebay.com/itm/125575167955"
encoded_url = urllib.parse.quote_plus(target_url)

# Scrape.do API endpoint
api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}&geocode=us"

response = requests.get(api_url)

print(response.status_code)

A successful response should print:

200

If you’re blocked or see status code 403, try these adjustments:

  • Append &render=true to enable JavaScript rendering
  • Check that super=true is included to enable premium proxies
  • Make sure your token is valid and hasn’t hit the limit

2. Parsing and Extracting Product Data

To extract data from an eBay product page, you first need to understand the structure of the page.

Open the product in your browser and use the DevTools panel to inspect the elements you’re after. Right-click on any field (like the product name or price), and select “Inspect”. This will highlight the exact HTML tag and attributes you can use to locate it in your script.

In this section, we’ll extract the following fields:

  • Product Name
  • Price
  • Product Image URL
  • Product ID (we won’t scrape this since it’s already in the URL)

2.1 Parse the Page with BeautifulSoup

Once you’ve made a successful request and received the page’s HTML, you can parse it using BeautifulSoup to make it easier to navigate:

from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "html.parser")

This gives you access to all the HTML elements in a tree-like structure, which you can search by tag names, class names, and attributes.

2.2 Get Product ID from the URL

In this case, we’re scraping a specific product, and the item ID is already part of the URL. So we don’t need to extract it from the page.

product_id = "125575167955"

For large-scale scraping, you’ll likely have a list of URLs that already include the product IDs and storing them ahead of time keeps your extraction logic simpler.

2.3 Extract Product Name

Let’s inspect the product title first. In the browser, locate the title using DevTools. You should see it inside an <h1> tag with a class like x-item-title__mainTitle.

This tag contains only the title text, so it’s safe to extract it directly:

try:
    title = soup.find("h1", class_="x-item-title__mainTitle").get_text(strip=True)
except Exception:
    title = None

We use a try-except block here to prevent the script from crashing if the element is missing or renamed which is useful when scraping hundreds of items.

2.4 Extract Price

Now let’s do the same for the price. Open DevTools and inspect the price text.

You’ll usually find it inside a <div> tag with the class x-price-primary.

try:
    price = soup.find("div", class_="x-price-primary").get_text(strip=True)
except Exception:
    price = None

This will return the full price string as shown on the page (e.g., US $44.99).

2.5 Extract Product Image URL

Product images on eBay are displayed in a carousel. When the page loads, the first image in the carousel is usually marked with the class active.

Each image is wrapped inside a <div> with the class ux-image-carousel-item, and the visible one will also have the active class.

The actual image is inside an <img> tag, so we’ll target that:

try:
    image_url = soup.select_one('.ux-image-carousel-item.active').find("img")["src"]
except Exception:
    image_url = None

This grabs the src attribute of the active image, which is a direct link to the image file.

To test the full setup, print the results:

print(title, "---", price, "---", product_id, "---", image_url)

Final code should look like this:

import requests
import urllib.parse
from bs4 import BeautifulSoup

# Scrape.do API token
TOKEN = "<your-token>"

# eBay product page
target_url = "https://www.ebay.com/itm/125575167955"
encoded_url = urllib.parse.quote_plus(target_url)

# Scrape.do API endpoint
api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}&geocode=us"

response = requests.get(api_url)
product_id = "125575167955"

soup = BeautifulSoup(response.text, "html.parser")

# Product Name
try:
    title = soup.find("h1", class_="x-item-title__mainTitle").get_text(strip=True)
except Exception:
    title = None

# Price
try:
    price = soup.find("div", class_="x-price-primary").get_text(strip=True)
except Exception:
    price = None

# Product Image
try:
    image_url = soup.select_one('.ux-image-carousel-item.active').find("img")["src"]
except Exception:
    image_url = None

print(title, "---", price, "---", product_id, "---", image_url)

And this will be your output:

3. Exporting the Data

Finally, let’s save the output into a CSV file.

We’ll create a simple CSV file with the following columns:

  • Product ID
  • Product Name
  • Price
  • Image URL

Import Python’s built-in csv module:

import csv

We’ll open a file named ebay_product.csv, write the headers, and then insert a single row with our data:

with open("ebay_product.csv", mode="w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["product_id", "title", "price", "image_url"])  # header row
    writer.writerow([product_id, title, price, image_url])          # data row

The newline="" ensures the output isn’t spaced out weirdly on Windows, and encoding="utf-8" avoids issues with special characters in product names.

Now run the final code and you should see a file named ebay_product.csv in your project directory.

Open it with any spreadsheet app or text editor and check that all fields are correctly placed.

You now have a working data extraction and export flow for a simple eBay product listing.

Scraping Product Data from an eBay Listing with Dropdown Variants

Some eBay product pages look deceptively simple like the one we just scraped.

Until you notice multiple dropdowns that create numerous variations for the same product.

Take for example this amazing St. Patrick’s Day t-shirt.

Unlike the product we scraped earlier, this one has multiple variations (size and color), and stock or price data of these variations are not visible unless you click on the dropdowns.

We could very well use the playWithBrowser feature of Scrape.do to click and select these dropdowns to find prices and stock info for each variation, but there’s an easier way.

eBay hides info for every variation inside a large JavaScript object called MSKU, which contains a complete nested map of all product variants, stock status, and pricing info. That’s what we’ll scrape.

In this section, we’ll:

  • Get the fully rendered HTML of an eBay product page
  • Extract and parse the MSKU JSON blob
  • Reconstruct all combinations of variations with price and availability
  • Export everything into a clean CSV

1. Send Request and Extract HTML

You’re familiar with everything below from the previous step.

Let’s first import required libraries, send a request, and get a clean HTML:

import json
import re
import csv
import requests
import urllib.parse

# Your Scrape.do API token
TOKEN = "<your-token>"

# The eBay product page URL
TARGET_URL = "https://www.ebay.com/itm/176212861437"

# Construct the Scrape.do URL
encoded_url = urllib.parse.quote_plus(TARGET_URL)
API_URL = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}&super=true"

# Send request through Scrape.do
response = requests.get(API_URL)
html_content = response.text

At this point, html_content contains the full HTML of the product page including the JavaScript variable that holds the variation data we need.

2. Extract and Parse MSKU Data

On eBay, products with dropdowns (like size or color) store all their variation logic inside a hidden JavaScript object called MSKU.

MSKU is unique to eBay, and means multi-variation SKU (SKU is pretty common, though).

This MSKU object includes:

  • All variation combinations (e.g., Small + Red, Medium + Blue)
  • Pricing and stock for each variation
  • The structure and labels of dropdown menus
  • Display names and internal IDs

You won’t find this data in the DOM, it’s buried inside a JavaScript variable somewhere in the page source, and looks like this (simplified):

"MSKU": {
  "variationsMap": { ... },
  "menuItemMap": { ... },
  "variationCombinations": { ... },
  "selectMenus": [ ... ]
}

To extract it, we’ll use a regex pattern that finds the "MSKU": { ... } block, stops cleanly before the next "QUANTITY" field, and avoids overreaching (non-greedy matching).

Here’s the code that does that:

msku_data = json.loads(re.search(r'"MSKU":({.+?}),"QUANTITY"', html_content).group(1))

Why we use .+? instead of .+:
Greedy matching (.+) would capture everything until the last "QUANTITY" in the file. Non-greedy (.+?) stops at the first one, giving us a clean JSON chunk.

3. Extract Core Data Structures

Now that we’ve parsed the full MSKU object, we can extract the key substructures inside it:

  • variationsMap: links variation IDs to their price and stock
  • menuItemMap: maps menu item IDs to display names
  • variationCombinations: maps menu selections (like “0_1”) to variation IDs

These are what we’ll use to reconstruct every possible product combination.

Here’s how we load them:

variations = msku_data.get('variationsMap', {})
menu_items = msku_data.get('menuItemMap', {})
variation_combinations = msku_data.get('variationCombinations', {})

We also grab the selection menu labels (like “Size” or “Color”) to use as CSV headers:

headers = [menu['displayLabel'] for menu in msku_data.get('selectMenus', [])] + ['Price', 'OutOfStock']

This prepares us to loop over each variation and extract the actual data in the next step.

4. Parse Each Variation

Now that we have the raw mappings and headers, we can walk through each possible variation combination.

Every entry in variationCombinations represents a unique combo of dropdown selections (like “Small + Red”) and links to a specific variation ID.

From there, we:

  • Map menu item IDs to their display names
  • Extract the price from binModel
  • Check availability via the outOfStock flag

⚠ You’ll need to dig deep into the JSON blob and understand exactly how it is structured to learn how we’re deconstructing variant data and then re-constructing it for the right variations.

Here’s the full loop:

rows = []
for combo_key, combo_data_id in variation_combinations.items():
    variation_data = variations.get(str(combo_data_id))
    if not variation_data:
        continue

    row = {}
    menu_ids = [int(i) for i in combo_key.split('_')]

    for menu_id in menu_ids:
        menu_item = menu_items.get(str(menu_id))
        if menu_item:
            for menu in msku_data.get('selectMenus', []):
                if menu_item['valueId'] in menu['menuItemValueIds']:
                    row[menu['displayLabel']] = menu_item['displayName']
                    break

    price_spans = variation_data.get('binModel', {}).get('price', {}).get('textSpans', [])
    row['Price'] = price_spans[0].get('text') if price_spans else None
    row['OutOfStock'] = variation_data.get('quantity', {}).get('outOfStock', False)

    rows.append(row)

At this point, rows contains every possible variation, neatly structured with dropdown labels, price, and stock info. All that’s left is writing it to a CSV.

5. Export to CSV

Once all product variations are parsed into rows, we can export everything into a structured CSV file for easy analysis.

Here’s the full code with export step added at the end:

import json
import re
import csv
import requests
import urllib.parse

# Configuration
TOKEN = "<your-token>"
TARGET_URL = "https://www.ebay.com/itm/176212861437"

# Scrape the page using scrape.do
response = requests.get(f"https://api.scrape.do/?url={urllib.parse.quote_plus(TARGET_URL)}&token={TOKEN}&super=true")
html_content = response.text

# Extract and parse MSKU data from HTML
msku_data = json.loads(re.search(r'"MSKU":({.+?}),"QUANTITY"', html_content).group(1))

# Get core data structures
variations = msku_data.get('variationsMap', {})
menu_items = msku_data.get('menuItemMap', {})
variation_combinations = msku_data.get('variationCombinations', {})

# Create CSV headers
headers = [menu['displayLabel'] for menu in msku_data.get('selectMenus', [])] + ['Price', 'OutOfStock']

# Process each variation combination
rows = []
for combo_key, combo_data_id in variation_combinations.items():
    variation_data = variations.get(str(combo_data_id))
    if not variation_data:
        continue

    row = {}
    menu_ids = [int(i) for i in combo_key.split('_')]

    # Extract menu values (size, color, etc.)
    for menu_id in menu_ids:
        menu_item = menu_items.get(str(menu_id))
        if menu_item:
            for menu in msku_data.get('selectMenus', []):
                if menu_item['valueId'] in menu['menuItemValueIds']:
                    row[menu['displayLabel']] = menu_item['displayName']
                    break

    # Extract price and stock status
    price_spans = variation_data.get('binModel', {}).get('price', {}).get('textSpans', [])
    row['Price'] = price_spans[0].get('text') if price_spans else None
    row['OutOfStock'] = variation_data.get('quantity', {}).get('outOfStock', False)

    rows.append(row)

# Write to CSV
with open('product_data.csv', 'w', newline='', encoding='utf-8') as csvfile:
    writer = csv.DictWriter(csvfile, fieldnames=headers)
    writer.writeheader()
    writer.writerows(rows)

print("Written to product_data.csv")

Once you run the script, it’ll create a clean CSV file like this:

Bit complex, yes, but works!

Scraping eBay Reviews for a Product

User reviews are one of the most valuable data points on eBay.

They offer real customer sentiment, highlight recurring issues, and can reveal details that aren’t obvious from product descriptions.

Rather than scraping reviews embedded in the product page, we’ll target eBay’s dedicated review pages, which are easier to parse and support full pagination. Our goal is to collect every review, not just the first few.

For this section, I’m picking a premium water bottle which has tens of reviews.

No idea why I picked this product, I was probably thirsty :)

We’ll:

  • Load all paginated review pages
  • Extract structured data (reviewer name, rating, title, date, and comment)
  • Export everything into a CSV for further analysis

1. Send a Request to the Review Page

We start from the dedicated review page instead of the listing page. It loads directly, doesn’t require JavaScript interaction, and returns all visible review data.

We’ll use Scrape.do to fetch the full HTML:

import requests
import urllib.parse
from bs4 import BeautifulSoup

TOKEN = "<your-token>"
start_url = "https://www.ebay.com/urw/Hydro-Flask-FXM334-Straw-Lid-Black/product-reviews/26031753680?_itm=204914180000"

encoded_url = urllib.parse.quote_plus(start_url)
api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}&super=true&geocode=us"

response = requests.get(api_url)
soup = BeautifulSoup(response.text, "html.parser")

2. Extract Review Fields

Each review is contained within a .ebay-review-section block. Inside it, we’ll find reviewer’s name, rating, and you guessed it, review.

The star rating is stored in the data-stars attribute like data-stars="5-0", so we split and extract just the number.

Here’s the extraction logic:

def extract_reviews(soup):
    reviews = soup.select(".ebay-review-section")
    data = []

    for r in reviews:
        try:
            reviewer = r.select_one(".review-item-author").get_text(strip=True)
        except:
            reviewer = None

        try:
            rating = r.select_one("div.star-rating")["data-stars"].split("-")[0]
        except:
            rating = None

        try:
            title = r.select_one(".review-item-title").get_text(strip=True)
        except:
            title = None

        try:
            date = r.select_one(".review-item-date").get_text(strip=True)
        except:
            date = None

        try:
            comment = r.select_one(".review-item-content").get_text(strip=True)
        except:
            comment = None

        data.append({
            "Reviewer": reviewer,
            "Rating": rating,
            "Title": title,
            "Date": date,
            "Comment": comment
        })

    return data

3. Detect and Follow Pagination

Each review page includes a “Next” button that links to the next set of reviews. The easiest way to detect it is by looking for a link with rel="next".

The link is already absolute, so we can use it directly:

def get_next_page(soup):
    try:
        return soup.select_one('a[rel="next"]')["href"]
    except:
        return None

4. Scrape All Pages in a Loop

With both review extraction and pagination detection in place, we can loop through all available pages until we reach the end.

This loop will keep scraping as long as there’s a valid next link on the page:

all_reviews = []
current_url = start_url

while current_url:
    encoded = urllib.parse.quote_plus(current_url)
    api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded}&super=true&geocode=us"

    response = requests.get(api_url)
    soup = BeautifulSoup(response.text, "html.parser")

    all_reviews.extend(extract_reviews(soup))

    current_url = get_next_page(soup)

5. Export Reviews to CSV

Once all pages are scraped, we’ll write the results to a CSV.

Here’s the full code with CSV exporting added:

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

TOKEN = "<your-token>"
start_url = "https://www.ebay.com/urw/Hydro-Flask-FXM334-Straw-Lid-Black/product-reviews/26031753680?_itm=204914180000"

def extract_reviews(soup):
    reviews = soup.select(".ebay-review-section")
    data = []

    for r in reviews:
        try:
            reviewer = r.select_one(".review-item-author").get_text(strip=True)
        except:
            reviewer = None

        try:
            rating = r.select_one("div.star-rating")["data-stars"].split("-")[0]
        except:
            rating = None

        try:
            title = r.select_one(".review-item-title").get_text(strip=True)
        except:
            title = None

        try:
            date = r.select_one(".review-item-date").get_text(strip=True)
        except:
            date = None

        try:
            comment = r.select_one(".review-item-content").get_text(strip=True)
        except:
            comment = None

        data.append({
            "Reviewer": reviewer,
            "Rating": rating,
            "Title": title,
            "Date": date,
            "Comment": comment
        })

    return data

def get_next_page(soup):
    try:
        return soup.select_one('a[rel="next"]')["href"]
    except:
        return None

all_reviews = []
current_url = start_url

while current_url:
    encoded = urllib.parse.quote_plus(current_url)
    api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded}&super=true&geocode=us"

    response = requests.get(api_url)
    soup = BeautifulSoup(response.text, "html.parser")

    all_reviews.extend(extract_reviews(soup))

    current_url = get_next_page(soup)

with open("ebay_paginated_reviews.csv", mode="w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["Reviewer", "Rating", "Title", "Date", "Comment"])
    writer.writeheader()
    for row in all_reviews:
        writer.writerow(row)

print("Saved all reviews to ebay_paginated_reviews.csv")

And here’s the CSV it created:

Scraping eBay Search Results

Scraping reviews and listing variants has not been a big of a deal so far.

But what if you want to scrape a full search results page?

That’s where things get interesting, you’ll need to extract a list of products, each with its own:

  • Name
  • Price
  • Image
  • Link to the product page

Let’s imagine that St. Patrick’s Day is right around the corner and I want to scrape all related t-shirt designs from eBay for competitive insights.

Here’s how I would do it:

1. Sending the First Request

Let’s start by making a request to the eBay search results page using Scrape.do.

import requests
import urllib.parse
from bs4 import BeautifulSoup

# Your Scrape.do token
TOKEN = "<your-token>"
query_url = "https://www.ebay.com/sch/i.html?_nkw=saint+patricks+tshirt"
encoded_url = urllib.parse.quote_plus(query_url)

# Scrape.do endpoint
api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}"

response = requests.get(api_url)
soup = BeautifulSoup(response.text, "html.parser")

2. Extracting Product Listings

eBay uses multiple layouts for search results, which makes our job slightly more difficult.

While some pages follow the older .s-item structure, others use a newer .s-card layout. Instead of hardcoding for one or the other, we’ll detect which layout is present and use the correct selectors accordingly.

From each listing we’ll extract name, price, image URL, and link to the actual product.

Here’s the layout-aware extraction logic:

if soup.select("li.s-card"):
    items = soup.select("li.s-card")
    layout = "s-card"
elif soup.select("li.s-item"):
    items = soup.select("li.s-item")
    layout = "s-item"
else:
    items = []
    layout = None

for item in items:
    try:
        title = item.select_one(".s-card__title" if layout == "s-card" else ".s-item__title").get_text(strip=True)
    except:
        title = None

    try:
        price_spans = item.select(".s-card__price" if layout == "s-card" else ".s-item__price")
        if len(price_spans) > 1:
            price = " ".join([span.get_text(strip=True) for span in price_spans])
        elif price_spans:
            price = price_spans[0].get_text(strip=True)
        else:
            price = None
    except:
        price = None

    try:
        image_url = item.select_one(".s-card__image" if layout == "s-card" else ".s-item__image-img")["src"]
    except:
        image_url = None

    try:
        link = item.select_one("a.su-link" if layout == "s-card" else "a.s-item__link")["href"]
    except:
        link = None

    results.append({
        "title": title,
        "price": price,
        "image_url": image_url,
        "link": link
    })

3. Navigating Pagination

To scrape beyond the first page, we need to follow the “Next” button and repeat the extraction process.

eBay includes pagination links at the bottom of the page, and the “Next” button is usually marked with the class pagination__next.

We’ll loop through pages by following that link until no more pages are available. The scraper also adapts to layout changes between pages.

Before showing you the full code, let’s make the final addition:

4. Exporting the Data

We also need to export this data to CSV, so we’ll use the same method as before.

With pagination and CSV writing added, here’s the full working code:

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

# Your Scrape.do token
TOKEN = "<your-token>"
results = []
current_url = "https://www.ebay.com/sch/i.html?_nkw=saint+patricks+tshirt"

while current_url:
    encoded_url = urllib.parse.quote_plus(current_url)
    api_url = f"https://api.scrape.do/?token={TOKEN}&url={encoded_url}&geocode=us&super=true&render=true"

    response = requests.get(api_url)
    soup = BeautifulSoup(response.text, "html.parser")

    # Detect which layout is being used
    if soup.select("li.s-card"):
        items = soup.select("li.s-card")
        layout = "s-card"
    elif soup.select("li.s-item"):
        items = soup.select("li.s-item")
        layout = "s-item"
    else:
        break

    for item in items:
        try:
            title = item.select_one(".s-card__title" if layout == "s-card" else ".s-item__title").get_text(strip=True)
        except:
            title = None

        try:
            price_spans = item.select(".s-card__price" if layout == "s-card" else ".s-item__price")
            if len(price_spans) > 1:
                price = " ".join([span.get_text(strip=True) for span in price_spans])
            elif price_spans:
                price = price_spans[0].get_text(strip=True)
            else:
                price = None
        except:
            price = None

        try:
            image_url = item.select_one(".s-card__image" if layout == "s-card" else ".s-item__image-img")["src"]
        except:
            image_url = None

        try:
            link = item.select_one("a.su-link" if layout == "s-card" else "a.s-item__link")["href"]
        except:
            link = None

        results.append({
            "title": title,
            "price": price,
            "image_url": image_url,
            "link": link
        })

    try:
        next_link = soup.select_one("a.pagination__next")["href"]
        current_url = next_link
    except:
        current_url = None

# Export to CSV
with open("ebay_search_results.csv", mode="w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "image_url", "link"])
    writer.writeheader()
    for row in results:
        writer.writerow(row)

print("Scraping completed. Results saved to ebay_search_results.csv.")

And this is going to be our large CSV file:

You’re now able to scrape any and all data from eBay.

Conclusion

You’re now able to scrape the entirety of eBay, including:

  • Static product pages using simple HTML parsing
  • Search results that span multiple layouts and pages
  • Variant-based listings by extracting hidden MSKU data from the HTML source
  • All reviews of any product

To make this process reliable, we used Scrape.do, which handled anti-bot detection and proxy management for us, enabling us to bypass geo-restrictions and blocks.

If you want to skip the hard parts of scraping, especially on JavaScript-heavy sites like eBay, Scrape.do is built for it.

👉 Try Scrape.do today and start scraping faster, cleaner, and without the constant breakage.