DEV Community

Cover image for Amazon's Anti-Bot Is ML-Based. Here's the Rotation Strategy That Survived 65 Runs.
Vhub Systems
Vhub Systems

Posted on • Edited on

Amazon's Anti-Bot Is ML-Based. Here's the Rotation Strategy That Survived 65 Runs.

Scraping Amazon Products with Python: A No-Nonsense Guide

As a senior Python developer who's built 35 web scrapers over the years, I've tangled with e-commerce giants like Amazon more times than I can count. Scraping product data from Amazon can be incredibly useful for market research, price tracking, or building datasets for machine learning models. But let's be real: it's not a walk in the park. Amazon's anti-scraping measures are aggressive, and if you're not careful, you'll hit blocks faster than you can say "CAPTCHA." In this tutorial, I'll walk you through the key techniques: spotting what triggers blocks, rotating sessions to evade detection, parsing pages with BeautifulSoup, and structuring your output with a simple JSON schema. I'll include two Python code examples, be upfront about the hard parts, and keep things practical. We're aiming for reliability, not perfection—scraping is always a cat-and-mouse game.

Before we dive in, a quick note on ethics and legality. Scraping Amazon violates their terms of service, and in some cases, it could lead to legal issues under laws like the Computer Fraud and Abuse Act (CFAA) in the US. Always check local regulations, use scraped data responsibly, and consider official APIs if available (Amazon has one, but it's not free for everything). This guide assumes you're doing this for educational or personal use—don't build a commercial scraper without proper permissions.

What Triggers Blocks on Amazon?

Amazon doesn't mess around with scrapers. They've got sophisticated bot detection powered by machine learning, monitoring everything from your request patterns to your IP address. Common triggers include:

  • Rapid-fire requests: Hitting the site too quickly without delays mimics bot behavior. Amazon rate-limits aggressively; exceed it, and you're looking at 503 errors or outright bans.

  • Missing or suspicious headers: If your requests don't look like they're coming from a real browser, you're flagged. Amazon checks User-Agent, Accept-Language, and other headers for authenticity.

  • Static IP or no proxy: Using the same IP for multiple requests screams "scraper." They track IPs and block data center proxies easily.

  • Behavioral anomalies: Things like not handling cookies properly, ignoring JavaScript rendering (Amazon pages often load dynamically), or failing to solve occasional CAPTCHAs.

The hard part here is that blocks aren't always immediate or consistent. Sometimes you'll scrape 100 pages fine, then bam—endless CAPTCHAs. Amazon evolves their detection, so what works today might fail tomorrow. Testing on a small scale is crucial; I've burned through proxies debugging this.

To mimic a real user, you need solid headers. Here's a Python dictionary of headers that I've used successfully (as of late 2024—test and rotate them):

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
    'Referer': 'https://www.amazon.com/',  # Change this based on the page
}
Enter fullscreen mode Exit fullscreen mode

These are pulled from a real Chrome session—use tools like Chrome DevTools to capture your own. But headers alone won't save you; combine them with session rotation, which we'll cover next.

Implementing Session Rotation

Session rotation is your best defense against IP-based blocks. The idea is to cycle through proxies, User-Agents, and even full sessions (including cookies) to make each request look like it's from a different user. Without this, you'll get blocked after 10-20 requests, especially on product search pages.

The hard truth: Proxies aren't cheap or foolproof. Free ones are worthless for Amazon—they're often blacklisted. Paid residential proxies (from providers like Bright Data or Oxylabs) work better because they look like real home IPs, but even those can fail if overused. Expect to rotate every 5-10 requests, and add random delays (1-5 seconds) to avoid patterns. Also, Amazon sometimes requires JavaScript execution for full page loads, so tools like Selenium might be needed for dynamic content, adding complexity and slowness.

Here's a Python code example using the requests library with proxy rotation. We'll assume you have a list of proxies (e.g., from a provider). This rotates sessions by creating new requests.Session objects.

import requests
import random
import time

# List of proxies (format: 'http://user:pass@ip:port' or just 'http://ip:port')
proxies_list = [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    # Add more...
]

# List of User-Agents to rotate
user_agents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
    # Add more...
]

def get_rotated_session():
    session = requests.Session()
    proxy = random.choice(proxies_list)
    session.proxies = {'http': proxy, 'https': proxy}
    session.headers.update({'User-Agent': random.choice(user_agents)})
    return session

def scrape_amazon_page(url, max_retries=3):
    for attempt in range(max_retries):
        session = get_rotated_session()
        try:
            response = session.get(url, timeout=10)
            if response.status_code == 200:
                return response.text
            elif response.status_code == 503:
                print(f"Rate limited. Retrying after delay...")
                time.sleep(random.uniform(5, 10))
            else:
                print(f"Error {response.status_code}. Rotating...")
        except requests.exceptions.RequestException as e:
            print(f"Request failed: {e}. Rotating...")
            time.sleep(random.uniform(1, 3))
    raise Exception("Failed to scrape after retries.")

# Example usage
url = 'https://www.amazon.com/s?k=laptop'
html = scrape_amazon_page(url)
print("Scraped HTML length:", len(html))
Enter fullscreen mode Exit fullscreen mode

This code creates a new session per request (or retry), picks a random proxy and User-Agent, and handles basic errors. In practice, you'd expand this with cookie management (e.g., persisting cookies across related requests) and more robust error handling. The downside? It's resource-intensive; running this at scale requires a proxy pool of at least 50-100 to avoid overuse. I've had scrapers fail mid-run because proxies got blocked en masse—always monitor and have backups.

Parsing with BeautifulSoup

Once you've got the HTML, parsing is where the magic happens. BeautifulSoup is my go-to for its simplicity and robustness against minor HTML changes. Amazon's product pages are messy, with data buried in divs and spans that shift occasionally. The hard part: Selectors break often due to A/B testing or updates. You'll need to inspect pages manually (use browser dev tools) and make your parser flexible—rely on classes or IDs, but have fallbacks.

We're targeting key fields: ASIN (unique product ID), title, price, rating, review count, and category. This example parses a product detail page, but you can adapt it for search results.

from bs4 import BeautifulSoup

def parse_amazon_product(html):
    soup = BeautifulSoup(html, 'html.parser')

    # ASIN (from data-asin attribute or URL)
    asin = soup.select_one('#ASIN')['value'] if soup.select_one('#ASIN') else None

    # Title
    title = soup.select_one('#productTitle')
    title_text = title.text.strip() if title else None

    # Price (handle variations)
    price = soup.select_one('.a-price .a-offscreen')
    price_text = price.text.strip() if price else None

    # Rating (stars)
    rating = soup.select_one('#acrPopover')['title'] if soup.select_one('#acrPopover') else None
    rating = rating.split(' ')[0] if rating else None  # e.g., '4.5'

    # Review count
    review_count = soup.select_one('#acrCustomerReviewText')
    review_text = review_count.text.strip().split(' ')[0] if review_count else None

    # Category (from breadcrumb)
    category = soup.select_one('#wayfinding-breadcrumbs_feature_div ul li:last-child a')
    category_text = category.text.strip() if category else None

    return {
        'asin': asin,
        'title': title_text,
        'price': price_text,
        'rating': rating,
        'review_count': review_text,
        'category': category_text
    }

# Example usage with scraped HTML
product_data = parse_amazon_product(html)
print(product_data)
Enter fullscreen mode Exit fullscreen mode

This extracts data using CSS selectors. It's straightforward but fragile—Amazon changes classes like #productTitle semi-frequently. Test on multiple pages, and consider regex fallbacks for price (e.g., handling currencies). For search pages, loop over result divs with soup.find_all('div', {'data-component-type': 's-search-result'}). The real challenge is incomplete data; some products lack ratings, so add error handling to avoid crashes.

Defining the JSON Schema

To keep your output structured and reusable, use a JSON schema. This ensures consistency, especially if you're storing data in a database or feeding it to an API. Here's a simple schema for our fields, using JSON Schema format for validation:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "asin": {
      "type": "string",
      "description": "Amazon Standard Identification Number"
    },
    "title": {
      "type": "string",
      "description": "Product title"
    },
    "price": {
      "type": "string",
      "description": "Product price as string (e.g., '$99.99')"
    },
    "rating": {
      "type": "string",
      "description": "Average rating (e.g., '4.5')"
    },
    "review_count": {
      "type": "string",
      "description": "Number of reviews (e.g., '1,234')"
    },
    "category": {
      "type": "string",
      "description": "Product category"
    }
  },
  "required": ["asin", "title"]
}
Enter fullscreen mode Exit fullscreen mode

You can validate your parsed dict against this using Python's jsonschema library. I kept it minimal—strings for everything to handle formatting quirks, with only essentials required. In a full scraper, you'd generate a list of these objects and dump to JSON.

Wrapping It Up: Challenges and Alternatives

Putting it all together: Rotate sessions to fetch HTML, parse with BeautifulSoup, validate against the schema, and store. Scale by multiprocessing requests, but watch for blocks. The hardest parts? Evasion tactics fail over time—I've rebuilt scrapers multiple times due to Amazon updates. Legal risks are real, and costs add up (proxies can run $50-200/month for decent volume).

If building from scratch feels overwhelming, check out pre-built tools. For instance, the Apify actor at https://apify.com/lanky_quantifier/amazon-product-scraper has seen 65 runs and handles much of this heavy lifting with built-in proxies and parsing.

What proxy provider for Amazon in 2025 — residential or ISP?


CAPTCHA Handling: When to Accept Defeat and Switch Tools

CAPTCHAs on Amazon fall into two categories: the soft kind (a checkbox challenge that disappears with the right headers and a residential IP) and the hard kind (image grids or hCaptcha that require a human or a solving service).

For the soft kind: fix your headers first. A residential proxy plus the full sec-* header set eliminates 70-80% of Amazon CAPTCHAs in my experience. They're friction, not a wall.

For the hard kind, you have three options:

Option Cost Speed Reliability
2captcha / Anti-Captcha API ~$1–3 per 1000 solves 15–30s per solve High
Playwright + stealth plugin $0 (compute cost) Slow (full browser) Medium
Pre-built actor with CAPTCHA handling Pay-per-run Fast High

My actual approach for production Amazon scraping in 2025: I don't fight CAPTCHAs at scale. The moment a job needs to solve more than ~50 CAPTCHAs per run, I switch to a service that already handles it — specifically the Apify actor I mentioned. The time spent debugging CAPTCHA evasion is not linear with your scrape volume. It compounds badly.

Structuring Amazon Data for Real Use Cases

The JSON schema I showed earlier covers basics. But if you're building something useful — a price tracker, a competitor monitor, a repricing tool — you want more structure around how products relate to each other over time. Here's what a minimal price history record looks like:

from datetime import datetime

def build_price_record(asin: str, parsed: dict) -> dict:
    """Wrap parsed product data with tracking metadata."""
    price_raw = parsed.get("price", "")
    # Normalize: "$1,299.99" → 1299.99
    price_numeric = None
    if price_raw:
        cleaned = price_raw.replace("$", "").replace(",", "").strip()
        try:
            price_numeric = float(cleaned)
        except ValueError:
            pass

    return {
        "asin": parsed.get("asin"),
        "title": parsed.get("title"),
        "price_string": price_raw,
        "price_usd": price_numeric,
        "rating": parsed.get("rating"),
        "review_count": parsed.get("review_count"),
        "category": parsed.get("category"),
        "scraped_at": datetime.utcnow().isoformat() + "Z",
        "scraper_version": "1.4"
    }
Enter fullscreen mode Exit fullscreen mode

The scraped_at timestamp and scraper_version fields seem trivial but they're what let you debug "why did this price spike on March 3rd?" six months from now. Add them from day one.


Ready to Run Amazon Scraping Without Setting Up Infrastructure?

If you're tracking prices, monitoring competitors, or building an e-commerce dataset — and you'd rather not fight Amazon's anti-bot measures yourself:

👉 E-commerce Price Intelligence Starter Pack — $19 — includes Amazon scraper, price comparison, and price drop alerts, all pre-configured and ready to deploy on Apify.

Or if you need a custom Amazon monitoring solution — specific categories, custom output schema, webhook alerts:

📩 vhubsystems@gmail.com | Hire on Upwork

Top comments (0)