Back to blog
ProxiesOct 21, 202610 min read

How to Scrape Amazon with Python (2026): Prices, Products & Search Results

Scrape Amazon prices, products and search results with Python. Tested in October 2026, with working selectors, US ZIP code targeting and block handling.

SimplyNode Team
Engineering & Support · SimplyNode
How to Scrape Amazon with Python (2026): Prices, Products & Search Results

Scrape Amazon with Python

How to Scrape Amazon with Python (2026 Guide)

Run a typical "scrape Amazon with Python" tutorial today and you'll probably get this:

Title: Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 6 ft, Black) Price: None Rating: 4.7 out of 5 stars Reviews: (90,152) Ships to: Ukraine

The title and rating come through, but the price is missing. Amazon decides what to show based on your delivery location, and it guesses that location from your IP. We ran this from a non-US server, so Amazon showed "This item cannot be shipped to your selected delivery location", and no price at all.

That's one of six things we found when we tested Amazon scraping in October 2026. This guide gives you code that works with today's Amazon: product pages, search results, prices for a specific US ZIP code, top reviews, and handling for the blocks you'll hit.

Quick answer

A minimal product scraper with requests and BeautifulSoup:

import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "Accept-Language": "en-US,en;q=0.9", } r = requests.get("https://www.amazon.com/dp/B088NRLMPV", headers=headers, timeout=30) soup = BeautifulSoup(r.text, "lxml") def text(selector): el = soup.select_one(selector) return el.get_text(" ", strip=True) if el else None print("Title: ", text("#productTitle")) print("Price: ", text("#corePrice_feature_div .a-offscreen")) print("Rating: ", text("#acrPopover span.a-icon-alt")) print("Reviews: ", text("#acrCustomerReviewText")) print("Ships to:", text("#glow-ingress-line2"))

If Price is None and Ships to isn't a US location, you need a US IP or a US ZIP code (see Step 3). For more than a handful of pages, you also need rotating residential proxies and block detection (see Step 5). The full script at the end does all of it.

We tested on amazon.com with Python 3.13, requests 2.34, beautifulsoup4 4.15 and curl_cffi 0.16, sending a few dozen requests spaced several seconds apart.

  1. Your location decides whether you see prices. From a non-US IP, product pages showed "Deliver to Ukraine" and no price. After setting ZIP code 10001 through Amazon's own "Deliver to" form, the same page showed $237.49 and "Only 1 left in stock".

  2. The default python-requests User-Agent gets a 503 right away. With browser-like headers, the same URL returned 200.

  3. Amazon sometimes returns a JavaScript challenge instead of HTML. The response is 200 OK but only ~2 KB of JavaScript (Akamai's bm-verify interstitial), with zero products. It appeared on some first requests and not on others. Status codes won't tell you: you have to check the content.

  4. CAPTCHAs come fast from a single IP. The second product page we requested from the same IP within a few seconds got the "Enter the characters you see below" page. That's what rotating proxies are for.

  5. A popular selector broke. h2 a span, used in several top-ranking tutorials for search result titles, now returns nothing. The title is in h2 span, and the link wraps the h2.

  6. The "See all reviews" page now requires sign-in. /product-reviews/{ASIN} redirects to the login page. Without an account, you get only the top reviews on the product page, and sometimes not even those.

Is it legal to scrape Amazon?

This isn't legal advice, but here's the practical line most teams follow:

  • Public data only. Prices, titles, ratings and product details visible without logging in are generally considered public. Don't scrape behind a login: it's clearly against Amazon's terms, and your account can be banned.

  • No personal data. Review texts are fine for sentiment analysis, but don't collect reviewer names or profile links. Under GDPR and similar laws, that's personal data you probably don't need.

  • Be gentle. Keep request rates reasonable, cache what you already have, and don't hammer the same pages.

  • Amazon's Conditions of Use restrict automated access. Read them and decide with your legal team, especially for commercial use.

Setup

pip install requests beautifulsoup4 lxml

That's all the full script needs. We used Python 3.13; any Python 3.10+ should work.

Step 1: Fetch the page and detect blocks

Amazon rarely tells you it blocked you with a clear status code. Check the HTML for known block pages:

BLOCK_MARKERS = ( "validateCaptcha", # "Enter the characters you see below" "bm-verify", # JavaScript interstitial challenge "api-services-support@amazon.com", # "Sorry! Something went wrong" (503 page) )

def _get(self, url, **kwargs): time.sleep(random.uniform(*self.delay)) r = self.session.get(url, timeout=30, **kwargs) if r.status_code in (429, 503) or any(m in r.text for m in BLOCK_MARKERS): raise Blocked(f"{r.status_code} on {url}") r.raise_for_status() return r.text

Without this check, your scraper will happily parse a CAPTCHA page, find no products, and save empty rows without any error.

Step 2: Parse a product page

These selectors worked on amazon.com product pages in October 2026:

Field

Selector

Example value

Title

#productTitle

Apple AirPods Pro 2 Wireless Earbuds…

Price

#corePrice_feature_div .a-offscreen (fallback: .a-price .a-offscreen)

$237.49

Rating

#acrPopover span.a-icon-alt

4.6 out of 5 stars

Review count

#acrCustomerReviewText

(47,013)

Availability

#availability

In Stock

Brand

#bylineInfo

Visit the Apple Store

Bullet points

#feature-bullets li

10 items

Main image (hi-res)

#landingImage → data-old-hires

…AC_SL1500.jpg

Delivery location

#glow-ingress-line2

New York 10001

Selectors that returned nothing in our tests but still appear in older tutorials: #priceblock_ourprice and #SalesRank.

Always save the delivery location next to the price. If a price looks wrong, the first thing to check is which location Amazon priced it for.

def parse_product(html, asin): soup = BeautifulSoup(html, "lxml") image = soup.select_one("#landingImage") return { "asin": asin, "title": text(soup, "#productTitle"), "price": to_price(text(soup, "#corePrice_feature_div .a-offscreen") or text(soup, ".a-price .a-offscreen")), "rating": to_rating(text(soup, "#acrPopover span.a-icon-alt")), "reviews": to_count(text(soup, "#acrCustomerReviewText")), "availability": text(soup, "#availability"), "brand": text(soup, "#bylineInfo"), "bullets": [li.get_text(" ", strip=True) for li in soup.select("#feature-bullets li")], "image": image.get("data-old-hires") if image else None, "delivery_location": text(soup, "#glow-ingress-line2"), "top_reviews": [...], # see Step 6 }

Small helpers turn Amazon's text into numbers: "$1,234.56" → 1234.56, "4.6 out of 5 stars" → 4.6, "(90.1K)" → 90100. They're in the full script.

Step 3: Set the delivery ZIP code to get real prices

Prices, availability, delivery dates and even search rankings depend on the delivery address. You have two levers:

  1. A US IP address. Amazon guesses your location from it. A US residential proxy gives you a realistic US location by default.

  2. An explicit ZIP code. The "Deliver to" button lets any visitor pick a ZIP without signing in. Your scraper can do the same three requests the browser does:

def set_zip_code(self, zip_code): # 1. Any product page has the "Deliver to" button with a CSRF token. html = self._get(f"{BASE_URL}/dp/B0D1XD1ZV3") button = BeautifulSoup(html, "lxml").select_one("#nav-global-location-data-modal-action") if button is None: raise Blocked("location button not found") modal = json.loads(button["data-a-modal"]) # 2. The location popup returns a second token. popup = self._get(BASE_URL + modal["url"], headers=modal["ajaxHeaders"]) token = re.search(r'CSRF_TOKEN\s*:\s*"([^"]+)"', popup) if token is None: raise Blocked("location token not found") # 3. Submit the ZIP code, exactly like the "Apply" button does. r = self.session.post( f"{BASE_URL}/portal-migration/hz/glow/address-change", params={"actionSource": "glow"}, json={"locationType": "LOCATION_INPUT", "zipCode": zip_code, "deviceType": "web", "storeContext": "generic", "pageType": "Detail", "actionSource": "glow"}, headers={"anti-csrftoken-a2z": token.group(1), "Accept": "application/json"}, timeout=30, ) if not r.ok or not r.json().get("successful"): raise RuntimeError(f"Could not set ZIP code {zip_code}: {r.text[:200]}")

In our test, the response confirmed "countryCode":"US","zipCode":"10001","city":"NEW YORK", and every page in that session showed "Deliver to New York 10001" with US prices.

The ZIP code is an Amazon setting, not a proxy feature. You don't need a proxy provider with ZIP-level targeting: we set ZIP 10001 from a non-US IP without any proxy at all. The proxy's job is different: a US residential IP keeps Amazon from blocking you and makes the session look like a normal US shopper. The ZIP then pins the exact delivery address.

The ZIP lives in the session cookies. Two consequences:

  • Use one session per location. To compare prices in New York and Los Angeles, run two scrapers with zip_code="10001" and zip_code="90001".

  • Keep the same IP for the whole session. Cookies that suddenly arrive from a different IP look suspicious. Use sticky proxy sessions (the same IP for several minutes) rather than a new IP on every request. When you get blocked, start a new session: new IP, new cookies, ZIP set again.

Step 4: Scrape search results

Search pages are where you find ASINs, track rankings and watch competitors' prices.

def parse_search(html): soup = BeautifulSoup(html, "lxml") results, seen = [], set() positions = {True: 0, False: 0} # ads and organic results are ranked separately for item in soup.select('div[data-component-type="s-search-result"]'): asin = item.get("data-asin") sponsored = item.select_one(".puis-sponsored-label-text") is not None # The same product can appear twice: as an ad and as an organic result. Keep both. if not asin or (asin, sponsored) in seen: continue seen.add((asin, sponsored)) positions[sponsored] += 1 results.append({ "position": positions[sponsored], "asin": asin, "title": text(item, "h2 span"), "url": f"{BASE_URL}/dp/{asin}", "price": to_price(text(item, ".a-price .a-offscreen")), "rating": to_rating(text(item, ".a-icon-alt")), "reviews": to_count(text(item, "a[href*='customerReviews'] span")), "sponsored": sponsored, "image": (item.select_one("img.s-image") or {}).get("src"), }) return results

Three details that most tutorials miss:

  • Ads and organic results are ranked separately. In our test for "usb c cable", the same LISEN cable was sponsored #1 and organic #2. If you deduplicate by ASIN only, you lose one of those positions, and rank tracking breaks.

  • Build the URL from the ASIN. Links on sponsored results go through an ad redirect (/sspa/click?...). https://www.amazon.com/dp/{ASIN} is clean and stable.

  • Don't read "list price" from search cards. In our test, .a-text-price on search cards held the price per unit (like $4.49/count), not the old price.

Pagination: add &page=2, &page=3 and so on to the search URL, or follow a.s-pagination-next. A page returned 16 to 22 results in our tests, depending on the layout Amazon served.

Step 5: Handle blocks with retries and proxies

From a single IP, we got a CAPTCHA on the second product page. At any real volume you need:

  1. Delays with jitter. The scraper waits 3–7 seconds between requests by default.

  2. Block detection (Step 1).

  3. A new identity on every block: new IP, new cookies, ZIP set again, then retry with exponential backoff.

def fetch(self, url, attempts=4): for attempt in range(attempts): try: return self._get(url) except (Blocked, requests.RequestException) as e: print(f" attempt {attempt + 1} failed: {e}") time.sleep(2 ** attempt * 5) try: self.new_session() except (Blocked, requests.RequestException): pass # the next attempt will try again raise RuntimeError(f"Giving up on {url}")

new_session() picks a random proxy from proxies.txt, one proxy URL per line:

http://USERNAME:PASSWORD@PROXY_HOST:PORT_1 http://USERNAME:PASSWORD@PROXY_HOST:PORT_2 http://USERNAME:PASSWORD@PROXY_HOST:PORT_3

Which proxies work for Amazon:

Proxy type

Result on Amazon

Datacenter

Blocked or CAPTCHA-ed quickly. Amazon knows cloud IP ranges.

Residential (US)

Best default. Real home ISP IPs, realistic US location, prices visible.

Mobile (4G/5G)

Highest trust, useful for the toughest volumes, but more expensive per GB.

With SimplyNode, set the location to United States (or a specific state or city), choose sticky rotation by time (10–30 minutes works well for a scraping session), and export the proxy list. Each line is a separate endpoint with its own IP, so new_session() switches to a different IP on every block. See proxies for Amazon for setup details.

Plain requests was enough in our tests. If you start seeing the JavaScript challenge (bm-verify) on most requests even through residential IPs, the next step is a real browser: see our Selenium proxy guide. A browser runs the challenge's JavaScript like any visitor would.

Step 6: Top reviews (without signing in)

Since the full reviews page now requires sign-in, the reviews you can get without an account are the ones shown on the product page itself, about a dozen. Some product pages don't include them in the initial HTML at all: in our tests, the same product had 13 reviews in one response and none in another.

"top_reviews": [ { # no reviewer names: that's personal data you don't need "rating": to_rating(text(r, "[data-hook=review-star-rating]")), "title": text(r, "[data-hook=reviewTitle]"), "body": text(r, "[data-hook=reviewRichContentContainer]"), "date": text(r, "[data-hook=review-date]"), "verified": r.select_one("[data-hook=avp-badge]") is not None, } for r in soup.select("[data-hook=review]") ],

Note the selectors: older tutorials use [data-hook=review-title] and [data-hook=review-body], which returned nothing in our tests. Today's are reviewTitle and reviewRichContentContainer.

Don't log in to scrape reviews. It's clearly against Amazon's terms, the account will likely be banned, and everything you collect is tied to your identity.

The full script

Save as amazon_scraper.py, optionally create proxies.txt, and run python amazon_scraper.py. It sets ZIP 10001, searches for "usb c cable", scrapes the top 3 organic products, and saves amazon_search.csv and amazon_products.json.

"""amazon_scraper.py: scrape Amazon search results and product pages with Python. Tested on amazon.com in October 2026. Amazon changes its HTML often: if a field comes back empty, check the selectors first. """ import csv import json import os import random import re import time import requests from bs4 import BeautifulSoup BASE_URL = "https://www.amazon.com" HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", } BLOCK_MARKERS = ( "validateCaptcha", # "Enter the characters you see below" "bm-verify", # JavaScript interstitial challenge "api-services-support@amazon.com", # "Sorry! Something went wrong" (503 page) ) class Blocked(Exception): pass def load_proxies(path="proxies.txt"): """One proxy URL per line, e.g. http://user:pass@host:port. Empty list = no proxy.""" if not os.path.exists(path): return [] with open(path) as f: return [line.strip() for line in f if line.strip()] class AmazonScraper: def __init__(self, zip_code="10001", proxies=None, delay=(3, 7)): self.zip_code = zip_code self.proxies = proxies or [] self.delay = delay self.session = None self.new_session() # --- session and location ------------------------------------------------- def new_session(self): """Fresh cookies, next proxy (= new IP), and the delivery ZIP set again.""" self.session = requests.Session() self.session.headers.update(HEADERS) if self.proxies: proxy = random.choice(self.proxies) self.session.proxies.update({"http": proxy, "https": proxy}) if self.zip_code: self.set_zip_code(self.zip_code) def set_zip_code(self, zip_code): # 1. Any product page has the "Deliver to" button with a CSRF token. html = self._get(f"{BASE_URL}/dp/B0D1XD1ZV3") button = BeautifulSoup(html, "lxml").select_one("#nav-global-location-data-modal-action") if button is None: raise Blocked("location button not found") modal = json.loads(button["data-a-modal"]) # 2. The location popup returns a second token. popup = self._get(BASE_URL + modal["url"], headers=modal["ajaxHeaders"]) token = re.search(r'CSRF_TOKEN\s*:\s*"([^"]+)"', popup) if token is None: raise Blocked("location token not found") # 3. Submit the ZIP code, exactly like the "Apply" button does. r = self.session.post( f"{BASE_URL}/portal-migration/hz/glow/address-change", params={"actionSource": "glow"}, json={"locationType": "LOCATION_INPUT", "zipCode": zip_code, "deviceType": "web", "storeContext": "generic", "pageType": "Detail", "actionSource": "glow"}, headers={"anti-csrftoken-a2z": token.group(1), "Accept": "application/json"}, timeout=30, ) if not r.ok or not r.json().get("successful"): raise RuntimeError(f"Could not set ZIP code {zip_code}: {r.text[:200]}") # --- fetching --------------------------------------------------------------- def _get(self, url, **kwargs): time.sleep(random.uniform(*self.delay)) r = self.session.get(url, timeout=30, **kwargs) if r.status_code in (429, 503) or any(m in r.text for m in BLOCK_MARKERS): raise Blocked(f"{r.status_code} on {url}") r.raise_for_status() return r.text def fetch(self, url, attempts=4): for attempt in range(attempts): try: return self._get(url) except (Blocked, requests.RequestException) as e: print(f" attempt {attempt + 1} failed: {e}") time.sleep(2 ** attempt * 5) try: self.new_session() except (Blocked, requests.RequestException): pass # the next attempt will try again raise RuntimeError(f"Giving up on {url}") # --- public API -------------------------------------------------------------- def search(self, query, page=1): html = self.fetch(f"{BASE_URL}/s?k={requests.utils.quote(query)}&page={page}") return parse_search(html) def product(self, asin): html = self.fetch(f"{BASE_URL}/dp/{asin}") return parse_product(html, asin) # --- parsing ---------------------------------------------------------------------- def text(node, selector): el = node.select_one(selector) return el.get_text(" ", strip=True) if el else None def to_price(value): if not value: return None match = re.search(r"[\d,]+\.?\d*", value) return float(match.group().replace(",", "")) if match else None def to_rating(value): match = re.search(r"\d+(\.\d+)?", value or "") return float(match.group()) if match else None def to_count(value): """'(47,013)' -> 47013, '(90.1K)' -> 90100""" match = re.search(r"([\d,.]+)\s*([KM]?)", value or "") if not match: return None number = float(match.group(1).replace(",", "")) return int(number * {"": 1, "K": 1_000, "M": 1_000_000}[match.group(2)]) def parse_search(html): soup = BeautifulSoup(html, "lxml") results, seen = [], set() positions = {True: 0, False: 0} # ads and organic results are ranked separately for item in soup.select('div[data-component-type="s-search-result"]'): asin = item.get("data-asin") sponsored = item.select_one(".puis-sponsored-label-text") is not None # The same product can appear twice: as an ad and as an organic result. Keep both. if not asin or (asin, sponsored) in seen: continue seen.add((asin, sponsored)) positions[sponsored] += 1 results.append({ "position": positions[sponsored], "asin": asin, "title": text(item, "h2 span"), "url": f"{BASE_URL}/dp/{asin}", "price": to_price(text(item, ".a-price .a-offscreen")), "rating": to_rating(text(item, ".a-icon-alt")), "reviews": to_count(text(item, "a[href*='customerReviews'] span")), "sponsored": sponsored, "image": (item.select_one("img.s-image") or {}).get("src"), }) return results def parse_product(html, asin): soup = BeautifulSoup(html, "lxml") image = soup.select_one("#landingImage") return { "asin": asin, "title": text(soup, "#productTitle"), "price": to_price(text(soup, "#corePrice_feature_div .a-offscreen") or text(soup, ".a-price .a-offscreen")), "rating": to_rating(text(soup, "#acrPopover span.a-icon-alt")), "reviews": to_count(text(soup, "#acrCustomerReviewText")), "availability": text(soup, "#availability"), "brand": text(soup, "#bylineInfo"), "bullets": [li.get_text(" ", strip=True) for li in soup.select("#feature-bullets li")], "image": image.get("data-old-hires") if image else None, "delivery_location": text(soup, "#glow-ingress-line2"), "top_reviews": [ { # no reviewer names: that's personal data you don't need "rating": to_rating(text(r, "[data-hook=review-star-rating]")), "title": text(r, "[data-hook=reviewTitle]"), "body": text(r, "[data-hook=reviewRichContentContainer]"), "date": text(r, "[data-hook=review-date]"), "verified": r.select_one("[data-hook=avp-badge]") is not None, } for r in soup.select("[data-hook=review]") ], } if __name__ == "__main__": scraper = AmazonScraper(zip_code="10001", proxies=load_proxies()) results = scraper.search("usb c cable") print(f"Found {len(results)} products") products = [] for item in [r for r in results if not r["sponsored"]][:3]: p = scraper.product(item["asin"]) print(f'{p["asin"]} ${p["price"]} {p["rating"]}★ ({p["reviews"]}) {p["title"][:60]}') products.append(p) with open("amazon_search.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=results[0].keys()) writer.writeheader() writer.writerows(results) with open("amazon_products.json", "w") as f: json.dump(products, f, indent=2, ensure_ascii=False)

Output from our test run (October 21, 2026):

Found 16 products B088NRLMPV $9.99 4.7★ (90152) Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, B088NMR44C $9.99 4.7★ (90152) Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, B0HJBWVSSS $6.99 5.0★ (1) Mcbazel USB-C Charging Cable for Wii U Gamepad, Type-C Femal

And the CSV:

position,asin,title,url,price,rating,reviews,sponsored,image 1,B088NRLMPV,"Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 6 ft, Black)",https://www.amazon.com/dp/B088NRLMPV,9.99,4.7,90100,False,https://m.media-amazon.com/images/I/71IE9dLBduL._AC_UY218_.jpg 2,B088NMR44C,"Anker USB C to USB C Cable, 60W Fast Charging Cable (2-Pack, 3.3 ft, Black)",https://www.amazon.com/dp/B088NMR44C,9.99,4.7,90100,False,https://m.media-amazon.com/images/I/71btI2mQBmL._AC_UY218_.jpg

Keep your scraper alive

Amazon changes its HTML often, and different visitors get different layouts. Some habits that save you from silently collecting empty data:

  • Monitor empty fields. If more than ~10% of products come back with title or price = None, stop and check the selectors. That's how we noticed h2 a span had broken.

  • Save the raw HTML of failed pages. You can't debug a CAPTCHA you didn't keep.

  • Cache. Prices change, but titles, images and bullet points don't change daily. Re-scrape those weekly, prices daily or hourly.

  • Scrape by ASIN. Once you know which products you track, go straight to /dp/{ASIN} instead of searching every time. It's fewer requests and less load.

  • Store the delivery location with every price. Without it, a price change and a location change look the same.

Estimate proxy traffic

Proxy traffic is measured on the wire, after compression. requests asks for gzip by default, so in our tests an amazon.com product page cost about 380 KB of traffic (1.4–1.9 MB once unpacked), and a search page about 160 KB. Plain requests downloads only the HTML, not images or scripts.

Product pages per day

Traffic per day

Traffic per month

1,000

~0.4 GB

~11 GB

10,000

~3.8 GB

~115 GB

100,000

~38 GB

~1.1 TB

That's roughly 2,700 product pages per GB. Add 10–20% for retries and the ZIP setup on each new session. A headless browser loads images and scripts too, so expect several times more traffic per page with Selenium or Playwright. More: How much proxy bandwidth do you actually need?

Troubleshooting

Symptom

Cause

Fix

Price is None, availability says “cannot be shipped”

Non-US location

Use a US residential IP and/or set a ZIP code (Step 3).

503 with “Sorry! Something went wrong”

Default User-Agent or IP flagged

Send browser headers. Rotate to a new IP.

200 OK but no products, ~2 KB response

JavaScript challenge (bm-verify)

Detect it (Step 1), retry from a new session and IP. If it persists, use a real browser.

“Enter the characters you see below”

CAPTCHA after too many requests per IP

Slow down, rotate IPs, new session per block.

Search titles empty

Old selector h2 a span

Use h2 span.

/product-reviews/ redirects to sign-in

Full reviews page requires login since late 2024

Use top reviews on the product page. Don’t log in.

Same ASIN twice in search results

Ad + organic placement

Keep both, track ad and organic positions separately.

Prices in the wrong currency

IP or ZIP for another country

Match the proxy country to the marketplace.

FAQ

Can you scrape Amazon with Python?

Yes. requests with browser-like headers and BeautifulSoup can parse Amazon product and search pages. For more than a few pages, you'll also need block detection, delays and rotating residential proxies, because Amazon shows CAPTCHAs quickly to a single IP.

Why does my Amazon scraper return no price?

Most often because Amazon thinks you're outside the US and can't ship there, so it doesn't show a price. Use a US IP, or set a US ZIP code through the "Deliver to" form, as shown in Step 3. Also check that you're not parsing a CAPTCHA page.

What are the current CSS selectors for Amazon prices?

On product pages, #corePrice_feature_div .a-offscreen, with .a-price .a-offscreen as a fallback. On search pages, .a-price .a-offscreen inside each div[data-component-type="s-search-result"]. We tested these in October 2026.

How do I scrape Amazon prices for a specific ZIP code?

Submit the ZIP through the same endpoint the "Deliver to" popup uses: read the CSRF token from the location button, load the popup, then POST the ZIP to /portal-migration/hz/glow/address-change. The location then sticks to your session cookies.

Can I scrape all Amazon reviews?

Not without signing in. Since late 2024, the full reviews page requires a login, and logging in to scrape violates Amazon's terms. Without an account, you can collect the top reviews shown on each product page.

Do I need proxies to scrape Amazon?

For anything beyond a few pages, yes. In our test, the second request from the same IP got a CAPTCHA. US residential proxies with sticky sessions work best: they give you a realistic US location and keep the same IP for a whole session.

Is it legal to scrape Amazon?

Scraping publicly visible data is generally treated differently from accessing data behind a login, but Amazon's terms restrict automated access. Collect only public data, skip personal data like reviewer names, keep request rates low, and check with a lawyer for commercial use.

Should I use Selenium or requests for Amazon?

Start with requests: it's faster and uses far less proxy traffic. Switch to a browser (Selenium or Playwright) only if the JavaScript challenge appears on most requests, or if you need data that only loads with JavaScript.


Scrape Amazon without blocks

Everything in this guide works best with US residential IPs. SimplyNode gives you:

  • 8M+ residential IPs, with US state and city targeting, so your scraper starts as a realistic US shopper (set the exact ZIP in code, as in Step 3)

  • Sticky sessions from 90 seconds to 6 hours, so a whole scraping session keeps one IP and its ZIP cookies

  • Unlimited concurrent sessions for parallel scrapers

  • Pay-as-you-go from $2.25/GB, with traffic that doesn't expire

Set up proxies for Amazon →

Related reading

SimplyNode Team
Oct 21, 2026
SN
SimplyNode Team
Engineering & Support · SimplyNode

The team behind the SimplyNode network - residential and mobile proxies, 8M+ ethically-sourced IPs, a 99.3% success rate. We write about the practical infrastructure work behind reliable scraping.

All articles by SimplyNode Team