Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

eBay Scraper

Extract eBay search listings and product pages as clean, normalized JSON, using the Scrapeless cloud Scraping Browser — no local Chrome, no proxy fleet.

Four interchangeable surfaces (Python, Node.js, CLI, MCP) emit the same JSON shape, documented in DATA_MODEL.md.

Use case

eBay is where resale and secondary-market pricing actually happens, so this is the data layer for:

  • Resale pricing — what a used model really sells for, not what a retailer lists it at.
  • Sneaker, collectible and parts arbitrage — spot listings priced below the going rate.
  • Competitor and seller monitoring — track a seller's catalogue, feedback rate and shipping terms.
  • Condition-aware market research — eBay exposes subtitles like "Pre-Owned" and "Open Box" that retail sites do not.

Unlike the Walmart sibling repo, extraction here is CSS-selector based — eBay renders listings server-side. The selectors carry .s-card__* primary and .s-item__* fallback paths, because eBay ships both card layouts depending on the query.

Requirements

Setup

git clone https://github.com/<owner>/ebay-scraper.git
cd ebay-scraper
cp .env.example .env          # then fill in SCRAPELESS_API_KEY
export SCRAPELESS_API_KEY=your_key_here

Python

cd browser/python
pip install scrapeless playwright parsel loguru python-dotenv
python run.py                             # prints JSON to stdout
SAVE_TEST_RESULTS=true python run.py      # writes results/*.json instead

Node.js

cd browser/nodejs
pnpm install
node run.mjs

Point it at your own targets without editing code:

EBAY_SAMPLE_SEARCH_URL="https://www.ebay.com/sch/i.html?_nkw=nintendo+switch&_ipg=60" \
EBAY_SAMPLE_PRODUCT_URL="https://www.ebay.com/itm/177439887865" python run.py

_ipg in the search URL controls page size (60 by default) and the scraper reads it to plan pagination — so change _ipg rather than looping pages yourself.

Surfaces

Surface Path Built on
Python browser/python official scrapeless SDK + Playwright over CDP
Node.js browser/nodejs official @scrapeless-ai/sdk + puppeteer-core over CDP
CLI browser/cli scrapeless-scraping-browser CLI with in-page eval
MCP browser/mcp Scrapeless MCP server — conversational, no code
Python Node.js
scrape_search(url, max_pages) scrapeSearch(url, maxPages)
scrape_product(url) scrapeProduct(url)

Expected output

A real run on 2026-08-12 (Python surface): 146 search listings for iphone 12 plus one product page, in 133 seconds. Committed samples in browser/python/results/ come from that run — untrimmed, because this scraper emits a normalized shape rather than a framework blob (58 KB for 146 listings).

search.json:

[
  {
    "url": "https://www.ebay.com/itm/116370846040",
    "title": "Apple iPhone 12 Unlocked Verizon, T-Mobile, AT&T all Carriers",
    "price": "$189.94",
    "shipping": "Free delivery in 2-4 days",
    "location": "United States",
    "subtitles": "Pre-Owned",
    "photo": "https://i.ebayimg.com/images/g/...",
    "rating": "95%",
    "rating_count": 128
  }
]

product.json adds id, seller_name, seller_url, photos[], a features key/value table, and variants[] — see DATA_MODEL.md.

Field coverage is not uniform

Measured across the 146 listings in that run:

Field Empty
url, title, price, shipping, photo 0
location, subtitles, rating 26 (18%)
rating_count 27 (18%)

Sellers without feedback history have no rating, and listings without a stated condition have no subtitles. Treat those four as nullable — the schema in DATA_MODEL.md marks them so, and price is the only field the parser requires (an item without one is skipped as a layout row).

Prices are display strings, not numbers

price arrives as "$189.94", and eBay also emits ranges ("US $10.00 to $20.00") on multi-variant listings. Parse before comparing:

import re

def to_amount(price: str | None) -> float | None:
    if not price:
        return None
    match = re.search(r"([\d,]+\.\d{2})", price)          # first amount of a range
    return float(match.group(1).replace(",", "")) if match else None

On product pages there are two price fields: price_original in the listing's currency, and price_converted, eBay's approximate conversion for the viewer's region. Which currency price_converted shows depends on the proxy country of the browser sessionDEFAULT_PROXY_COUNTRY = "US" in the scraper. Change the proxy country and that field changes with it, so store price_original as your source of truth.

Tests

cd browser/python && poetry install --with dev && poetry run pytest    # needs pytest-asyncio + pytest-rerunfailures
cd browser/nodejs && node --test test.mjs

The Python suite validates each record against the documented schema and skips itself when SCRAPELESS_API_KEY is unset, so it is safe in CI without a key.

Troubleshooting

Symptom Cause and fix
Zero listings parsed eBay served the alternate card layout. The selectors already carry .s-item__* fallbacks; if both miss, the markup changed — check .s-card__price first, since price gates the whole record.
Hangs, then net:: or ERR_TUNNEL_CONNECTION_FAILED Transient CDN teardown; the code retries these.
shipping holds a long blob instead of a delivery line Seen on older captures, not in the 2026-08-12 run. eBay sometimes renders a screen-reader span with the whole card summary; if it returns, take the shortest matching text node.
price_converted is in an unexpected currency It follows the session's proxy country. See above.
Fewer pages than expected eBay caps deep pagination; the scraper stops when a page yields no items.

Verification status

  • Verified live on 2026-08-12 — the Python surface completed a full run in 133 s: 146 search listings plus one product page, no block and no CAPTCHA. The committed browser/python/results/ fixtures come from that run.
  • Not re-run in this pass: the Node.js, CLI and MCP surfaces. They were carried over unchanged from the upstream monorepo and share the data model, but their committed fixtures are older than the Python ones.

Project structure

ebay-scraper/
├── DATA_MODEL.md                 # the JSON contract every surface emits
├── browser/
│   ├── python/                   # SDK + Playwright over CDP, with pytest validators
│   ├── nodejs/                   # SDK + puppeteer-core over CDP
│   ├── cli/                      # scrapeless-scraping-browser CLI
│   └── mcp/                      # Scrapeless MCP server config
├── .env.example
└── LICENSE

Related

License

MIT — see LICENSE.

About

Scrape eBay product and search listings as structured JSON with the Scrapeless Scraping Browser — Python, Node.js, CLI, and MCP surfaces.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages