Build a local Amazon.com search scraper with one Python file. Selenium controls a separate Chrome profile, BeautifulSoup reads each result card and Flask returns the data as JSON.
This tutorial builds a small local API that searches Amazon.com and returns up to 10 product cards as JSON. You need Python, Chrome, Selenium, BeautifulSoup and Flask. The browser runs with a separate Chrome profile, so the scraper never opens your everyday profile.
The result is a learning project, not an anti-bot bypass. Amazon can change its HTML, show a challenge or deny access. Use the code only where you have permission, keep the request rate low and stop when the site denies access.
What you will build
You will create one file named app.py. When it is running, this URL starts a search:
http://127.0.0.1:5000/search?query=mechanical%20keyboard
The response contains a title, product URL, image URL and price when those fields are present. Some listings do not show every field, so the code uses null for missing values instead of crashing.
Before you start
You need Python 3 and Google Chrome on a Windows computer. The same Python code works on macOS and Linux, but the command that starts Chrome uses a different executable path.
Chrome changed remote debugging in version 136. The --remote-debugging-port switch is ignored for the default Chrome data directory, so you must also provide a separate --user-data-dir. Google recommends this separation and recommends Chrome for Testing for browser automation. Read the Chrome remote debugging notice if you want the security details.
Do not copy cookies, saved passwords or profile files from your normal Chrome profile. This tutorial does not need them.
Step 1: create the project
Open PowerShell and run these commands one line at a time:
mkdir amazon-scraper
cd amazon-scraper
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install selenium beautifulsoup4 flask
The virtual environment keeps this project's packages away from your other Python projects. Current Selenium releases include Selenium Manager, which can find and manage ChromeDriver when you call webdriver.Chrome(). You normally do not need to download ChromeDriver by hand. The Selenium Manager documentation explains the fallback behavior.
Step 2: start an isolated Chrome profile
Close any old automation window. Then run this command in PowerShell:
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9333 `
--user-data-dir="C:\selenium-profiles\amazon"
Leave that Chrome window open. Chrome creates the directory if it does not exist. Port 9333 is only a local meeting point between Selenium and this browser.
If PowerShell says that the file does not exist, check this second common Chrome location:
& "$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9333 `
--user-data-dir="C:\selenium-profiles\amazon"
Step 3: add the Python code
Create app.py inside the amazon-scraper folder and paste in this complete example:
from decimal import Decimal, InvalidOperation
from urllib.parse import quote_plus, urljoin
from bs4 import BeautifulSoup
from flask import Flask, jsonify, request
from selenium import webdriver
from selenium.common.exceptions import TimeoutException, WebDriverException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
app = Flask(__name__)
AMAZON_BASE_URL = "https://www.amazon.com"
DEBUGGER_ADDRESS = "127.0.0.1:9333"
driver = None
def get_driver():
global driver
if driver is None:
options = Options()
options.debugger_address = DEBUGGER_ADDRESS
driver = webdriver.Chrome(options=options)
return driver
def parse_usd(text):
cleaned = text.replace("$", "").replace(",", "").strip()
try:
return float(Decimal(cleaned))
except InvalidOperation:
return None
def search_amazon(query):
browser = get_driver()
url = f"{AMAZON_BASE_URL}/s?k={quote_plus(query)}"
browser.get(url)
result_selector = ''
WebDriverWait(browser, 15).until(
EC.presence_of_element_located((By.CSS_SELECTOR, result_selector))
)
soup = BeautifulSoup(browser.page_source, "html.parser")
products = []
for item in soup.select(result_selector):
title_element = item.select_one("h2")
link_element = title_element.find_parent("a") if title_element else None
price_element = item.select_one(".a-price .a-offscreen")
image_element = item.select_one("img.s-image")
if not title_element or not link_element:
continue
href = link_element.get("href")
if not href:
continue
price_text = price_element.get_text(strip=True) if price_element else None
image_url = image_element.get("src") if image_element else None
products.append(
{
"asin": item.get("data-asin") or None,
"title": title_element.get_text(" ", strip=True),
"url": urljoin(AMAZON_BASE_URL, href),
"price_text": price_text,
"price_usd": parse_usd(price_text) if price_text else None,
"image_url": image_url,
}
)
if len(products) == 10:
break
return products
@app.get("/search")
def search():
query = request.args.get("query", "").strip()
if not query:
return jsonify({"error": "Add a query parameter, for example ?query=keyboard"}), 400
try:
products = search_amazon(query)
return jsonify({"query": query, "count": len(products), "results": products})
except TimeoutException:
return jsonify(
{
"error": "No search results appeared within 15 seconds. Check the open Chrome window for an error or challenge page."
}
), 503
except WebDriverException:
return jsonify(
{
"error": "Selenium could not connect to Chrome. Start Chrome with remote debugging on port 9333, then try again."
}
), 503
if __name__ == "__main__":
app.run(
host="127.0.0.1",
port=5000,
debug=False,
threaded=False,
use_reloader=False,
)
Step 4: run the scraper
Make sure the isolated Chrome window is still open. In the same PowerShell window where you activated the virtual environment, run:
python app.py
Open a second PowerShell window and test the API:
curl.exe "http://127.0.0.1:5000/search?query=mechanical%20keyboard"
A successful response has this shape. Your products and field values will be different:
{
"query": "mechanical keyboard",
"count": 10,
"results": [
{
"asin": "EXAMPLE123",
"title": "Example product title",
"url": "https://www.amazon.com/example",
"price_text": "$89.99",
"price_usd": 89.99,
"image_url": "https://example.com/image.jpg"
}
]
}
What each part of the code does
get_driver() connects Selenium to the Chrome window you started on port 9333. It keeps one connection because this small app processes one request at a time.
search_amazon() opens an Amazon.com search URL. quote_plus() safely puts the search phrase into that URL. Selenium waits for a product result before BeautifulSoup reads the page HTML.
The wait checks for a real page condition instead of sleeping for a fixed five seconds. Selenium's own guidance describes fixed sleeps as a common source of unreliable automation and recommends explicit waits for specific conditions. See Selenium waiting strategies.
The loop reads each result card. It skips a card only when the title or link is missing. Price and image are optional. This matters because sponsored cards and page experiments do not always have identical markup.
parse_usd() is intentionally narrow. It parses prices from Amazon.com that use a dollar sign and a decimal point. Do not reuse it for other currencies without adding locale-aware parsing.
The Flask route reads ?query=... with request.args.get() and returns JSON. This follows the request and response patterns in the Flask quickstart.
Common errors and simple fixes
Selenium could not connect to Chrome
Chrome is not running on port 9333, or it was started without the separate user-data directory. Run the command from step 2 again and keep that browser window open.
Selenium cannot find ChromeDriver
Upgrade Selenium first:
python -m pip install --upgrade selenium
Selenium Manager may need internet access the first time it resolves a compatible driver. On a company network, a proxy or firewall can block that download.
No results appeared within 15 seconds
Look at the isolated Chrome window. You may see a network error, a challenge page or a layout that no longer matches the selector. Do not add CAPTCHA solving or try to hide the automation. Stop the request and inspect the page you are allowed to access.
Some products have a null price or image
That is expected. Search cards vary, and some offers do not show a price until another action happens. Keep missing fields as null unless your application has a clear rule for them.
The selectors will change
The selectors in this example were reviewed on September 22, 2026, but Amazon does not publish them as an API contract. A selector that works today can fail after a page experiment or redesign.
When the result is empty, save the HTML you are authorized to inspect and check these four selectors first:
for the complete result cardh2for the product title.a-price .a-offscreenfor the visible price textimg.s-imagefor the product image
Change one selector at a time. Then run the same test query again. This is easier to debug than changing browser code, parsing and the API route together.
When this small scraper is a reasonable fit
Use this setup for learning, a local prototype or a small authorized check where you can watch the browser. It is deliberately limited to one browser and one request at a time.
It is a poor fit for a server, parallel requests, unattended jobs or a product that needs a stable response contract. The Flask development server in this example is also for local development, not public hosting.
A simpler option for production data
If your application needs recurring Amazon product data, compare the maintenance cost with a managed API before building more scraper infrastructure. The Amazon Instant Data API on RapidAPI provides a separate product interface. Check its current endpoints, coverage and pricing against your exact requirements before choosing it.
If you need a response designed for your application, describe the source and fields you need. Include the country storefront, required fields and expected request volume.
Your next step
Run one query and inspect the JSON. If it works, save your installed package versions:
python -m pip freeze > requirements.txt
The larger companion project on GitHub shows how to split browser control, parsing and routes into separate files. Keep the debug port in that project consistent with the port used to start Chrome.