Extract eBay search listings and product pages as clean, normalized JSON, using the Scrapeless cloud Scraping Browser — no local Chrome, no proxy fleet.
Three interchangeable surfaces (Python, Node.js and CLI) emit the same JSON shape, documented in DATA_MODEL.md.
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
subtitleslike "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.
- A Scrapeless API key — create a free account
- Python 3.10+ or Node.js 18+ (each surface stands alone)
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_herecd 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 insteadcd browser/nodejs
pnpm install
node run.mjsPoint 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.
| 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 |
| Python | Node.js |
|---|---|
scrape_search(url, max_pages) |
scrapeSearch(url, maxPages) |
scrape_product(url) |
scrapeProduct(url) |
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.
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).
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 NoneOn 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 session — DEFAULT_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.
cd browser/python && poetry install --with dev && poetry run pytest # needs pytest-asyncio + pytest-rerunfailures
cd browser/nodejs && node --test test.mjsThe 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.
| 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. |
- 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 and CLI 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.
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
├── .env.example
└── LICENSE
- Product: Scraping Browser · Scraping API
- Guide: eBay scraper with the Scrapeless Scraping Browser
MIT — see LICENSE.