What is product data scraping?
Product data scraping is the automated collection of structured information from ecommerce websites, retailer pages, marketplaces, and online catalogs.
A scraper reads page information and converts it into consistent fields such as product name, brand, SKU, category, current and original price, currency, availability, description, specifications, variants, seller information, rating and review count, image URL, product URL, and collection timestamp.
Ecommerce source
- Product name
- Brand
- SKU
- Price
- Availability
- Variants
- Seller
- Rating
- URL
Product Data Scraping
Selects and structures required fields
Structured dataset
- Spreadsheet
- CSV
- JSON
- Database
- API
- Data feed
The output may be a spreadsheet, CSV, JSON, database table, API response, or recurring data feed—either for a one-time catalog or for monitoring prices, stock, promotions, sellers, and assortment changes.
Start with the dataset, not the website
A common mistake is opening developer tools before deciding what the finished dataset should contain. Start by answering:
01
Which pages must be covered?
02
Which fields are required?
03
What does one row represent?
04
How will the data be used?
Product, variant, offer, and observation
These entities should not be treated as interchangeable. A product is the general item; a variant is a configuration such as size and color; an offer is a seller-specific listing; an observation is the information collected at a particular time and location.
Entity model
01
Product
`shoe-model-100`
Name, brand, category, description
02
Variant
`shoe-model-100-black-10`
Color, size, SKU, GTIN
03
Offer
`seller-a-offer-482`
Seller, price, shipping, stock
04
Observation
`offer-482-2026-07-29`
Collected price, availability, timestamp
| Entity | Example identifier | Typical fields |
|---|---|---|
| Product | `shoe-model-100` | Name, brand, category, description |
| Variant | `shoe-model-100-black-10` | Color, size, SKU, GTIN |
| Offer | `seller-a-offer-482` | Seller, price, shipping, stock |
| Observation | `offer-482-2026-07-29` | Collected price, availability, timestamp |
What product fields should you extract?
The correct fields depend on the business objective. Common groups include identity (name, brand, SKU, GTIN, product ID, URL), classification, pricing, availability and fulfillment, attributes, marketplace information, customer-feedback fields, and collection metadata such as timestamp, country, store, scrape status, and error status.
Identity
- Name
- Brand
- SKU
- GTIN
- Product ID
- URL
Classification
- Category
Pricing
- Current price
- Original price
- Currency
Availability
- Availability
- Fulfillment
Attributes
- Description
- Specifications
- Variants
- Image URL
Marketplace
- Seller information
Customer Feedback
- Rating
- Review count
Collection Metadata
- Timestamp
- Country
- Store
- Scrape status
- Error status
Do not collect every visible field simply because it is available. Extra fields increase extraction complexity, storage, validation, and maintenance.
Check how the website loads its product data
- Static HTML — Product information is in the HTML returned by the server; a normal HTTP request may be enough.
- JSON-LD structured data — Many pages include Schema.org Product data inside a
script type="application/ld+json"element. - Embedded JSON — Product data may sit in JavaScript variables or framework state.
- Network requests — Prices, inventory, variants, or reviews may load from an endpoint after the initial page.
- JavaScript-rendered content — The initial HTML may be a shell; Playwright or similar browser automation may be required.
Static HTML
Product information is in the HTML returned by the server; a normal HTTP request may be enough.
JSON-LD
Many pages include Schema.org Product data inside an application/ld+json script element.
Embedded JSON
Product data may sit in JavaScript variables or framework state.
Network Requests
Prices, inventory, variants, or reviews may load from an endpoint after the initial page.
JavaScript-Rendered
The initial HTML may be a shell; Playwright or similar browser automation may be required.
Prefer the simplest reliable source
- 01Structured data in HTML
- 02Static selectors
- 03Approved page data responses
- 04Browser rendering
Use the simplest method that reliably returns the required permitted data.
How to scrape one product page with Python
The following example uses fictional selectors. Replace them only after inspecting a permitted target page.
Install the libraries
Bash
pip install requests beautifulsoup4Basic scraper
Python
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from decimal import Decimal, InvalidOperation
from typing import Optional
import requests
from bs4 import BeautifulSoup
@dataclass
class Product:
name: str
url: str
sku: Optional[str]
price: Optional[str]
availability: Optional[str]
def clean_text(value: Optional[str]) -> Optional[str]:
if value is None:
return None
cleaned = " ".join(value.split())
return cleaned or None
def parse_price(value: Optional[str]) -> Optional[str]:
if not value:
return None
numeric = (
value.replace(",", "")
.replace("$", "")
.replace("€", "")
.replace("£", "")
.strip()
)
try:
return str(Decimal(numeric))
except InvalidOperation:
return None
def scrape_product(url: str) -> Product:
headers = {
"User-Agent": (
"Mozilla/5.0 (compatible; ProductResearchBot/1.0; "
"+https://your-company.example/bot-information)"
)
}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
name_element = soup.select_one("h1.product-name")
sku_element = soup.select_one("[data-product-sku]")
price_element = soup.select_one(".product-price")
availability_element = soup.select_one(".availability")
if name_element is None:
raise ValueError("Required product name was not found")
return Product(
name=clean_text(name_element.get_text()) or "",
url=url,
sku=(
clean_text(sku_element.get("data-product-sku"))
if sku_element
else None
),
price=parse_price(
clean_text(price_element.get_text())
if price_element
else None
),
availability=(
clean_text(availability_element.get_text())
if availability_element
else None
),
)
def main() -> None:
product_url = "https://store.example/products/demo-running-shoe"
try:
product = scrape_product(product_url)
except requests.RequestException as exc:
raise SystemExit(f"Request failed: {exc}") from exc
except ValueError as exc:
raise SystemExit(f"Extraction failed: {exc}") from exc
print(json.dumps(asdict(product), indent=2))
if __name__ == "__main__":
main()A possible result would be:
JSON
{
"name": "Demo Running Shoe",
"url": "https://store.example/products/demo-running-shoe",
"sku": "DRS-100-BLK",
"price": "79.99",
"availability": "In stock"
}This output is illustrative. It is not evidence from a live Nenodata extraction project.
The script can extract one permitted static page, but it does not yet find other product URLs, process pagination, run JavaScript, extract variants, retry failures, normalize currencies, detect duplicates, validate catalog completeness, monitor layout changes, or save historical observations.
Extract JSON-LD before relying on CSS selectors
CSS selectors can change when a website redesigns. Structured product data may provide a cleaner source when it is present and complete.
CSS selectors
Layout-coupled
Selectors can change when a website redesigns. Useful when structured data is incomplete or absent.
JSON-LD
Structure-first
Schema.org Product data may provide a cleaner source when present and complete. Still compare with the visible page.
- Product Page
- ld+json
- Schema.org Product
- Fields
- Validate
Confirm prices, variants, stock, and sellers before relying on structured data alone.
Python
from __future__ import annotations
import json
from typing import Any, Optional
from bs4 import BeautifulSoup
def find_product_json_ld(html: str) -> Optional[dict[str, Any]]:
soup = BeautifulSoup(html, "html.parser")
scripts = soup.select('script[type="application/ld+json"]')
for script in scripts:
raw_json = script.string
if not raw_json:
continue
try:
data = json.loads(raw_json)
except json.JSONDecodeError:
continue
candidates = data if isinstance(data, list) else [data]
for candidate in candidates:
if not isinstance(candidate, dict):
continue
if candidate.get("@type") == "Product":
return candidate
graph = candidate.get("@graph", [])
if isinstance(graph, list):
for item in graph:
if isinstance(item, dict) and item.get("@type") == "Product":
return item
return NoneDo not assume JSON-LD contains every required field. Compare it with the visible page and confirm whether prices, variants, stock, and sellers are represented correctly.
Export product data to CSV
- Python objects
- Rows
- CSV
Monitoring fields
- collected_at
- currency
- location
- seller
- variant ID
- scrape status
Python
import csv
from dataclasses import asdict
from pathlib import Path
from typing import Iterable
def save_products_to_csv(
products: Iterable[Product],
output_path: str,
) -> None:
rows = [asdict(product) for product in products]
if not rows:
raise ValueError("No products were provided")
path = Path(output_path)
with path.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)For recurring monitoring, add fields such as collected_at, currency, location, seller, variant ID, and scrape status. Without timestamps and context, a price record cannot reliably support historical analysis.
How to find every product on a website
Product URLs may be found through category and subcategory pages, pagination, infinite scrolling, XML sitemaps, internal search, brand pages, seller storefronts, approved product feeds, or public catalog endpoints.
Discovery paths
- Category pages
- Subcategories
- Pagination
- Infinite scroll
- XML sitemaps
- Internal search
- Brand pages
- Seller storefronts
- Approved feeds
- Public catalog endpoints
Catalog flow
- 1Homepage
- 2Categories
- 3Product URLs
- 4Canonicalization
- 5Deduplication
- 6Extraction queue
Follow category pagination
Python
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
def discover_product_urls(start_url: str) -> set[str]:
discovered: set[str] = set()
visited_pages: set[str] = set()
next_url: str | None = start_url
while next_url and next_url not in visited_pages:
visited_pages.add(next_url)
response = requests.get(next_url, timeout=30)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
for link in soup.select("a.product-card-link[href]"):
absolute_url = urljoin(next_url, link["href"])
discovered.add(absolute_url)
next_element = soup.select_one("a.next-page[href]")
next_url = (
urljoin(next_url, next_element["href"])
if next_element
else None
)
return discoveredNormalize canonical product URLs carefully. Remove only parameters confirmed not to change product identity or location context—a parameter may control size, seller, store, language, or delivery location.
Reconcile expected and discovered products: displayed count, unique URLs discovered, attempted, successful, failed, empty, duplicate, product/variant/offer records, and removed products. Agree the data model before interpreting row counts.
Reconciliation funnel
Labels only — replace with approved run metrics before presenting as evidence.
- Displayed count
- Unique URLs
- Attempted
- Successful
- Failed
- Empty
- Duplicate
- Products / Variants / Offers
How to scrape JavaScript-rendered products
- Open browser
- Load page
- Wait for meaningful content
- Extract fields
- Close browser
Install Playwright and browsers:
Bash
pip install playwright
playwright installPython
from __future__ import annotations
import json
from playwright.sync_api import (
TimeoutError as PlaywrightTimeoutError,
sync_playwright,
)
def scrape_dynamic_product(url: str) -> dict[str, str | None]:
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
try:
page = browser.new_page(viewport={"width": 1440, "height": 900})
page.goto(url, wait_until="domcontentloaded", timeout=45_000)
page.locator("h1.product-name").wait_for(state="visible", timeout=15_000)
name = page.locator("h1.product-name").inner_text()
price_locator = page.locator(".product-price")
stock_locator = page.locator(".availability")
return {
"name": name.strip(),
"price": (
price_locator.inner_text().strip()
if price_locator.count()
else None
),
"availability": (
stock_locator.inner_text().strip()
if stock_locator.count()
else None
),
"url": page.url,
}
finally:
browser.close()
def main() -> None:
try:
product = scrape_dynamic_product(
"https://store.example/products/demo-running-shoe"
)
except PlaywrightTimeoutError as exc:
raise SystemExit(
"The required product content did not become visible"
) from exc
print(json.dumps(product, indent=2))
if __name__ == "__main__":
main()Wait for meaningful content—title visible, price element present, loading indicator gone, expected network response, or minimum product-card count—rather than a fixed sleep.
Prefer structured data in HTML, then static selectors, then approved page data responses, then browser rendering. Use the simplest method that reliably returns the required permitted data.
Handling product variants
Colors, sizes, packs, conditions, and sellers do not necessarily mean distinct products. Choose one row per product, per variant, or per seller offer based on the use case.
Variant hierarchy
Product · shoe-model-100
Variant · black / size 10
Variant · black / size 11
Variant · white / size 10
Choose one row per product, per variant, or per seller offer based on the use case.
JSON
{
"product_id": "shoe-model-100",
"variant_id": "shoe-model-100-black-10",
"sku": "DRS-100-BLK-10",
"color": "Black",
"size": "10",
"price": 79.99,
"currency": "USD",
"availability": "in_stock"
}Incorrect
Price from variant A + stock status from variant B
Mixing attributes across variants produces invalid observations.
Correct
Separate variant IDs with matching attributes, price, and availability
Variant selectors often update several parts of the page at once—capture a consistent state.
Do not combine a price from one variant with the stock status of another. Variant selectors often update several parts of the page at once.
Location-dependent product data
Price, availability, delivery estimates, promotions, and seller offers may depend on country, ZIP code, store, currency, language, customer status, or collection time. Store location alongside the result—otherwise a later “price change” could actually be a location change.
Location-dependent fields
Price, availability, delivery estimates, promotions, and seller offers may change with context.
- Country
- ZIP code
- Store
- Currency
- Language
- Customer status
- Collection time
Clean and normalize the data
Keep separate fields for price text, numeric amount, and currency. Map availability into an agreed classification while retaining original text. Normalize units and pack quantities before comparing prices. Preserve raw source values so normalization can be corrected later.
Normalization
Raw
- "$1,299.00"
- "In Stock — ships today"
Normalized
- amount: 1299.00 · currency: USD · display kept
- availability: in_stock · original text retained
Preserve raw source values so normalization can be corrected later.
How to validate a product scrape
Evaluate URL coverage, required-field completeness, duplicates (canonical URL, SKU, GTIN, product/variant/seller keys—not names alone), range and format checks, run-over-time comparisons, and manual sample review after initial development or redesigns.
Extracted product records
- URL coverage
- Required fields
- Duplicates
- Range / format
- Run compare
- Manual review
Validated product dataset
Illustrative validation report
| Field | Records checked | Missing | Status |
|---|---|---|---|
| Product name | 1,000 | 0 | Pass |
| Product URL | 1,000 | 0 | Pass |
| SKU | 1,000 | 83 | Review |
| Price | 1,000 | 21 | Review |
| Availability | 1,000 | 7 | Review |
These figures are illustrative and are not Nenodata performance results. Replace with an approved run report before presenting as original evidence.
Common product-scraping problems
No products returned
Save returned HTML and compare with the browser.
Only the first page
Record every category page visited and reconcile counts.
Missing prices
Define which price is required (variant, seller, location, promotion).
Duplicates
Normalize canonical URLs and use stable identifiers.
Variants overwrite one another
Assign separate variant IDs and attributes.
Sudden breakage
Retain run logs, failed HTML, screenshots, and completeness alerts.
Inflated row counts
Document the row model and report products, variants, offers, and observations separately.
Product-scraping methods compared
Manual copy and paste
- Setup:
- Low initially
- Skill:
- Low
- Coverage:
- Poor
- Maintenance:
- High manual effort
- Best for:
- A few records
Browser extension
- Setup:
- Low
- Skill:
- Low
- Coverage:
- Limited to moderate
- Maintenance:
- User-managed
- Best for:
- Small, occasional projects
No-code scraper
- Setup:
- Low to moderate
- Skill:
- Low
- Coverage:
- Moderate
- Maintenance:
- Tool and user-managed
- Best for:
- Standard page structures
Custom Python script
- Setup:
- Moderate
- Skill:
- Medium to high
- Coverage:
- Moderate to high
- Maintenance:
- Internal team
- Best for:
- Controlled technical projects
Scraping API
- Setup:
- Moderate
- Skill:
- Medium
- Coverage:
- High where supported
- Maintenance:
- Shared with provider
- Best for:
- Teams wanting programmatic access
Managed extraction service
- Setup:
- Higher scoping effort
- Skill:
- Low customer coding requirement
- Coverage:
- Designed around project scope
- Maintenance:
- Provider-managed
- Best for:
- Recurring or complex requirements
Hybrid workflow
- Setup:
- Variable
- Skill:
- Medium
- Coverage:
- High
- Maintenance:
- Shared
- Best for:
- Teams needing standard and custom coverage
Decision framework
- A few URLs, one-time collection → Manual or browser extension may fit.
- Standard layouts, occasional runs → No-code tools may fit.
- Controlled technical project with static HTML → Custom Python script may fit.
- Programmatic access preferred → Evaluate a scraping API.
- Recurring, multi-site, or complex requirements → Evaluate managed extraction or a hybrid workflow.
No single method is best for every project. Match method to volume, refresh needs, and maintenance capacity.
No single method is best for every project. A browser extension may suit 50 URLs collected once; it may be inefficient for a multi-country catalog refreshed daily into a warehouse.
When to build internally or use a managed service
An internal script may fit a stable, limited, temporary project with a simple schema and straightforward delivery—if your team can own maintenance. Account for development, testing, infrastructure, monitoring, validation, documentation, and staff continuity.
A managed service may be more practical when many sites must be covered, catalogs change often, JavaScript rendering or complex matching is required, prices vary by location, refreshes are recurring, failures need remediation, or output must feed APIs, warehouses, or BI tools.
Build internally
May fit when
- The source is stable and limited
- The schema is simple
- Delivery is straightforward
- Your team can own maintenance
Account for development, testing, infrastructure, monitoring, validation, documentation, and staff continuity.
Managed service
May fit when
- Many sites must be covered
- Catalogs or layouts change often
- JavaScript rendering or complex matching is required
- Prices vary by location or refreshes are recurring
- Output must feed APIs, warehouses, or BI tools
Nenodata lists ecommerce product-catalog extraction, seller analytics, inventory tracking, review aggregation, price history, and category insights among its capabilities, with cleaning, normalization, validation, deduplication, and scheduled delivery. Exact coverage must be assessed per source. See ecommerce data extraction services and how Nenodata extracts and delivers data.
Responsible product-data collection
Before collecting data, consider public accessibility, authentication, applicable terms, personal data, copyright and database rights, request impact, intended use, jurisdictions, and whether permission or legal review is required. Public accessibility alone does not answer every question about permitted collection or reuse.
Turning product data into a recurring pipeline
01
Connect
Approved sources, locations, page types, access conditions
02
Extract
Product, variant, offer, and observation fields
03
Transform
Clean, normalize, validate, deduplicate, map to schema
04
Deliver
CSV, Excel, JSON, API, webhook, database, warehouse, or dashboard
For price monitoring, preserve product identity, offer context, availability, location, and collection time. See ecommerce price scraping and competitor price intelligence.
Product-scraping requirements checklist
Before developing or commissioning a scraper, document:
Product-scraping requirements checklist
Sources
- Domains
- Example URLs
- Countries
- Stores
- Auth
- Exclusions
Data
- Required / optional fields
- Identifiers
- Variant rules
- Seller rules
- Location rules
Coverage
- Expected counts
- Pagination
- Variants
- Sellers
- Removed products
Schedule
- One-time or recurring
- Refresh frequency
- Maximum data age
Quality
- Thresholds
- Duplicates
- Validation
- Failure handling
- Reconciliation
Delivery
- Format
- Schema
- Time zone
- Schedule
- Retention
Governance
- Intended use
- Source approval
- Restrictions
- Review ownership
How Nenodata can support product-data extraction
A basic script can suit a small, stable source. More complex requirements usually need discovery, schema planning, validation, scheduling, delivery, and maintenance. The practical starting point is to define target websites, example URLs, required fields, product and variant rules, locations, refresh frequency, expected output, and validation requirements.
Nenodata can then assess the source and propose a scoped workflow. Do not promise coverage, speed, accuracy, or real-time delivery until targets have been tested and approved.
Frequently asked questions
What is the easiest way to scrape products from a website?
For a small static page, a Python request and an HTML parser may be enough. For a few pages without coding, a browser extension or no-code tool may be easier. JavaScript-heavy, recurring, or multi-site projects may require browser automation, an API, or a managed extraction workflow.
Can I scrape website products into Excel?
Yes. Extract the required fields, store each record as a row, and export the results as CSV or XLSX. Define whether each row represents a product, variant, seller offer, or historical observation before exporting.
How do I scrape all products rather than one page?
Create a product-discovery process that follows categories, pagination, infinite scroll, sitemaps, or approved search endpoints. Deduplicate canonical URLs and reconcile the discovered count against the source.
Can Beautiful Soup scrape JavaScript websites?
Beautiful Soup parses HTML or XML; it does not execute JavaScript. If the required content appears only after JavaScript runs, use an appropriate data response or a browser automation tool such as Playwright.
How do I scrape product variants?
Identify every valid attribute combination, select or request each variant, and store its identifiers, attributes, price, and availability separately. Do not assume the default page state represents every variant.
How can I remove duplicate products?
Use canonical URLs and stable identifiers such as SKU, GTIN, EAN, UPC, MPN, or source product ID. Product names alone are not reliable duplicate keys.
How do I know whether the scraped dataset is complete?
Compare discovered, attempted, successful, failed, excluded, and duplicate URLs. Measure required-field completeness and separate product, variant, offer, and observation counts.
What is the difference between a scraper and a scraping API?
A scraper is the collection logic itself. A scraping API provides programmatic access to extraction or page-retrieval capabilities through requests and responses. The exact division of rendering, parsing, maintenance, validation, and delivery depends on the provider.
When should a business use a managed scraping service?
Consider a managed service when sources are complex, extraction is recurring, validation matters, several sites or locations are involved, or the data must feed business systems reliably.
Is product web scraping legal?
There is no universal answer. It depends on the source, access method, data, intended use, applicable terms, contractual obligations, and jurisdiction. Obtain qualified legal guidance for material or high-risk projects.
Sources used
- Beautiful Soup documentation — HTML/XML parsing and element selection
- Playwright for Python documentation — installation, browsers, and automation APIs
- Schema.org Product — structured vocabulary for products
- Nenodata services, How It Works, Ecommerce Price Scraping, and Price Intelligence