Tutorial11 min read

Web Scraping With Selenium and Python

Short answer

To scrape a website with Selenium and Python, install the selenium package, start headless Chrome with --headless=new, open the page, wait for the elements with WebDriverWait, read them with find_elements(By.CSS_SELECTOR, ...), scroll or click for more, and save the rows. Selenium is the right tool when a page builds its content with JavaScript or needs clicks; for plain HTML pages, Requests is faster. If find_element() raises NoSuchElementException for an element you can see, Selenium looked too early, in the wrong frame or outside a shadow root: the fixes are below. Selenium sends its traffic through ProxyField residential proxies with one browser flag (IP allowlist) or a username and password setup, from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.

residential IPs
412M+
from, traffic never expires
$1.95/GB
Easy Trial
1 GB for $2.95
On this page 14 sections
  1. Quick start: a headless Chrome scraper in 20 lines
  2. When Selenium is the right tool
  3. Step 1. Install Selenium in Python
  4. Step 2. Start headless Chrome
  5. Step 3. Locate elements
  6. Step 4. Wait for pages and elements to load
  7. Why Selenium raises NoSuchElementException
  8. Step 5. Scrape infinite scroll
  9. Step 6. Take screenshots
  10. Step 7. Hand the HTML to a parser and save the data
  11. Make Selenium scraping faster
  12. SeleniumBase and other helpers
  13. Use a proxy with Selenium
  14. Web Scraping With Selenium and Python FAQ

Quick start: a headless Chrome scraper in 20 lines

Shell
python3 -m pip install -U selenium
Python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")         # Chrome without a window
options.add_argument("--window-size=1366,900")

with webdriver.Chrome(options=options) as driver:   # quits Chrome even on errors
    driver.get("https://quotes.toscrape.com/js/")    # a practice page built by JavaScript
    WebDriverWait(driver, 10).until(
        EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.quote"))
    )
    print(driver.title)
    for q in driver.find_elements(By.CSS_SELECTOR, "div.quote")[:3]:
        print(q.find_element(By.CSS_SELECTOR, "small.author").text, "|",
              q.find_element(By.CSS_SELECTOR, "span.text").text[:50])

Output, run on 2026-10-27 with Selenium 4.51.0, Python 3.12 and Chrome 155 on Linux:

Quotes to Scrape
Albert Einstein | “The world as we have created it is a process of o
J.K. Rowling | “It is our choices, Harry, that show what we truly
Albert Einstein | “There are only two ways to live your life. One is

You need only Chrome and the package: Selenium Manager downloads the matching chromedriver on the first run. The wait matters: without it, find_elements() can run before the page's JavaScript has added the quotes. The seven steps below take the same script further: locating, waiting, infinite scroll, screenshots and saving to CSV.

When Selenium is the right tool

Selenium drives a real browser through WebDriver, the W3C standard that Chrome, Firefox, Edge and Safari implement. A browser runs the page's JavaScript, so Selenium sees what a visitor sees after the scripts have run. That makes it a good fit for:

  • pages that load their content with JavaScript and have no JSON request you can call directly;
  • data behind clicks, tabs, "load more" buttons or infinite scroll;
  • screenshots of pages as rendered;
  • teams that already run Selenium for testing and want one tool.

It is a poor fit for large crawls of static pages: a browser costs far more CPU, memory and traffic per page than an HTTP request. For those, see Python web scraping with Requests or Scrapy. Selenium in general: what Selenium is.

Step 1. Install Selenium in Python

Shell
python3 -m venv .venv && source .venv/bin/activate
pip install selenium

That is all: since Selenium 4.6, Selenium Manager finds or downloads the matching driver (chromedriver, geckodriver) when it is missing, so you no longer download drivers by hand (Selenium Manager docs). The current Python package is 4.51.0, which needs Python 3.10 or newer (PyPI).

Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.

Step 2. Start headless Chrome

Python
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--headless=new")        # no window; drop it to watch the browser
options.add_argument("--window-size=1366,900")
driver = webdriver.Chrome(options=options)
driver.get("https://quotes.toscrape.com/js/")  # a practice page rendered by JavaScript
print(driver.title)

--headless=new runs the full Chrome without a window. Selenium's Chrome options page lists it with --start-maximized and --user-data-dir=... among the commonly used arguments (Selenium: Chrome options), and the project recommends it over the old headless mode (Selenium blog: Headless is going away). Since Chrome 132, plain --headless means the same, because the old mode ships only as the separate binary chrome-headless-shell (Chrome docs). Set a window size: headless Chrome otherwise starts small, and some sites show a mobile layout. Background on headless mode: what a headless browser is.

Troubleshooting headless Chrome:

SymptomCauseFix
SessionNotCreatedException at webdriver.Chrome()Chrome and chromedriver differ: they "must match the major version" (Selenium docs)Remove the old chromedriver from your PATH and let Selenium Manager fetch the right one; update Selenium if Chrome is very new
Chrome crashes at start in Docker or as root (DevToolsActivePort file doesn't exist)No sandbox available, or a small /dev/shmAdd --no-sandbox and --disable-dev-shm-usage, only inside a container
A mobile layout, or elements missing that you see on desktopThe default headless window is small--window-size=1366,900 or larger
NoSuchElementException on content you can see in your browserThe script looked before the JavaScript ran, or in the wrong frameThe fixes below

By default Selenium 4 works with Chrome 75 and newer.

Step 3. Locate elements

Selenium 4 has two methods, find_element() (the first match, or an error) and find_elements() (a list, empty if nothing matches), each with a By strategy.

Python
from selenium.webdriver.common.by import By

quotes = driver.find_elements(By.CSS_SELECTOR, "div.quote")
for q in quotes:
    text = q.find_element(By.CSS_SELECTOR, "span.text").text
    author = q.find_element(By.CSS_SELECTOR, "small.author").text
    tags = [t.text for t in q.find_elements(By.CSS_SELECTOR, "a.tag")]
    print(author, "|", text[:60], tags)

next_link = driver.find_element(By.XPATH, "//li[@class='next']/a")
print(next_link.get_attribute("href"))
StrategyExampleUse it for
By.CSS_SELECTOR"div.quote span.text"Most lookups: short and fast
By.XPATH"//a[contains(text(), 'Next')]"Finding by text, or moving up the tree
By.ID, By.NAME"search"Form fields
By.LINK_TEXT, By.PARTIAL_LINK_TEXT"Next"Links by their visible text
By.TAG_NAME, By.CLASS_NAME"h1", "price"Simple cases

The old find_element_by_css_selector() style methods were removed in Selenium 4; code that uses them needs the By form above. Writing good selectors: HTML scraping with CSS selectors and XPath.

find_element() raises NoSuchElementException when nothing matches; find_elements() returns an empty list instead. Why that happens to elements you can see: the section after step 4.

Step 4. Wait for pages and elements to load

driver.get() returns when the page's load event fires, but JavaScript often adds content later. Selenium offers two waits (Selenium: waiting strategies):

  • Explicit wait (recommended): wait for one condition on one element, up to a timeout.
  • Implicit wait: a global setting that makes every find_element() retry for up to N seconds. Do not mix it with explicit waits, because the timeouts add up in hard-to-predict ways.
Python
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 15)
wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.quote")))   # in the DOM
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "li.next a")))  # visible and enabled
button.click()
wait.until(EC.staleness_of(quotes[0]))   # the old content is gone: the next page has loaded

Common conditions: presence_of_element_located, visibility_of_element_located ("wait until visible"), element_to_be_clickable, text_to_be_present_in_element, url_contains. Avoid time.sleep() for waiting on content: it is either too short or wastes seconds on every page.

Why Selenium raises NoSuchElementException

This is the error, as Chrome 155 and Selenium 4.51.0 printed it when our script looked for the quotes before the page's JavaScript had added them:

selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"div.quote"}
  (Session info: chrome=155.0.8059.79); For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#nosuchelementexception

Selenium's own definition: "The element can not be found at the exact moment you attempted to locate it" (Selenium: understanding common errors). Seeing the element in your browser does not mean Selenium can see it at that moment, in that document. Check the five causes in this order:

#CauseHow to tellFix
1Too early: JavaScript adds the element after get() returnsIt works when you add time.sleep(5)An explicit wait for that element
2Inside an iframe: the element is in another documentDevTools shows an <iframe> above it in the Elements treedriver.switch_to.frame(...), then back with switch_to.default_content()
3Inside a shadow root: a web component hides its DOMDevTools shows #shadow-root above itFind the host, then search in host.shadow_root
4Wrong page or window: a click opened a new tab, a redirect or a cookie banner page came firstdriver.current_url and driver.window_handlesSwitch with driver.switch_to.window(...), or close the cookie banner first
5The locator differs: the class is generated, the text is added later, or the headless page has a different layoutdocument.querySelectorAll("div.quote").length in the console returns 0A stable attribute (id, name, data-*), and the same window size as your browser

The fixes for causes 1 to 4, each tested on a local page that put an element in an iframe, one in a shadow root and one 2 seconds after load (all three raised NoSuchElementException before the fix):

Python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 1. Too early: wait up to 15 s for the element instead of failing at once
el = WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "div.late"))
)

# 2. Iframe: switch into the frame, read, switch back
driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, "iframe#prices"))
price = driver.find_element(By.CSS_SELECTOR, "span.price").text
driver.switch_to.default_content()

# 3. Shadow DOM: search inside the host's shadow root
host = driver.find_element(By.CSS_SELECTOR, "my-card")
name = host.shadow_root.find_element(By.CSS_SELECTOR, "p.name").text

# 4. New tab: switch to the newest window
driver.switch_to.window(driver.window_handles[-1])

driver.switch_to.frame() also takes a frame's name, id or index; EC.frame_to_be_available_and_switch_to_it(...) waits for the frame and switches in one step.

The errors people confuse with it

ExceptionWhat it means (Selenium docs)Fix
NoSuchElementExceptionThe element can't be found at that momentThe five causes above
TimeoutExceptionAn explicit wait ran out, or a page took longer than the page load timeout (300 s by default)Check that the condition can come true (the locator, the frame); raise the timeout or change the page load strategy
StaleElementReferenceExceptionAn element you found earlier can't be accessed now: the page refreshed, its DOM changed, or you switched window or frameFind the element again right before you use it
NoSuchFrameExceptionswitch_to.frame() could not find the frameWait for the frame; switch through each parent of a nested frame
NoSuchShadowRootExceptionThe element has no shadow root, or it is not attached yetTarget the shadow host; wait for the root
ElementClickInterceptedExceptionAnother element, such as a banner or modal, would get the clickWait for element_to_be_clickable, close the overlay, or scroll the element into view
ElementNotInteractableExceptionThe element is hidden or can't take that actionScroll it into view, wait for it to be visible, or pick the right element

Step 5. Scrape infinite scroll

Infinite-scroll pages load more items when you reach the bottom. Scroll with JavaScript, wait for new items, and stop when the page height stops growing.

Python
import time

driver.get("https://quotes.toscrape.com/scroll")
WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.quote")))
last = 0
while True:
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(1.5)                                   # give the next batch time to load
    count = len(driver.find_elements(By.CSS_SELECTOR, "div.quote"))
    if count == last:
        break
    last = count
print(last, "quotes loaded")

Before you scroll at all, open the Network tab: infinite scroll almost always calls a JSON endpoint with a page or offset parameter. Calling that endpoint with Requests gets the same data without a browser.

Step 6. Take screenshots

Python
driver.save_screenshot("page.png")                                  # the visible viewport
driver.find_element(By.CSS_SELECTOR, "div.quote").screenshot("first-quote.png")  # one element

For a full-page image in Chrome, set the window height to the page height before the screenshot (driver.set_window_size(1366, driver.execute_script("return document.body.scrollHeight"))); Firefox's driver has driver.get_full_page_screenshot_as_file("full.png").

Step 7. Hand the HTML to a parser and save the data

Selenium's element lookups make one WebDriver call each, which adds up on large pages. A faster pattern: let the browser render, then parse driver.page_source with Beautiful Soup or lxml.

Python
import csv
from bs4 import BeautifulSoup

soup = BeautifulSoup(driver.page_source, "html.parser")
rows = [{"text": q.select_one("span.text").get_text(strip=True),
         "author": q.select_one("small.author").get_text(strip=True)} for q in soup.select("div.quote")]
with open("quotes.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.DictWriter(f, fieldnames=["text", "author"])
    w.writeheader()
    w.writerows(rows)
driver.quit()

Always call driver.quit(), also on errors (try / finally), or orphaned browsers will eat the server's memory.

Make Selenium scraping faster

Selenium is slow mostly because a browser is slow. What helps:

  • Do not load images. options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2}) cuts load time and traffic.
  • Use the eager page load strategy (options.page_load_strategy = "eager") so get() returns when the HTML is parsed, not when every image has loaded.
  • Reuse one driver for many pages instead of starting a browser per URL.
  • Parse page_source once instead of hundreds of find_element() calls.
  • Run several drivers in parallel, a few per CPU core, or spread them with Selenium Grid.
  • Use the browser only where needed: fetch listing pages with Requests and open Selenium only for the pages that need JavaScript.

SeleniumBase and other helpers

SeleniumBase (4.56.4, MIT, October 2026) is a framework on top of Selenium for Python with built-in waits, simpler methods such as sb.open() and sb.click(), a test runner and extra browser modes. It suits teams who want Selenium's browser coverage with less boilerplate. Other ways to drive a browser from Python, including Playwright: Puppeteer, Selenium and Playwright alternatives and best headless browsers.

Use a proxy with Selenium

Chrome's --proxy-server flag takes an address but no username and password, so the short form works with IP allowlist authentication: add your server's IP to your account's allowlist, then pass the port you set up in the dashboard for it as <port>:

Python
options.add_argument("--proxy-server=http://gate.proxyfield.io:<port>")   # IP allowlist auth

With the IP allowlist, the product, location and session are set per port in the dashboard. With a username and password, city targeting goes in the username, as in <login>-country-us-city-new_york for New York. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional. For username and password authentication, rotation without restarting the browser, Firefox and SOCKS5, follow the Selenium proxy setup guide. With ProxyField each request can leave from a new residential IP, or a sticky session keeps one IP from 30 s to 36 h for flows that must stay on one address, with country, region and city targeting. Residential proxies cost $2.95/GB for 1 GB and fall to $1.95/GB from 1,000 GB, with no subscription; traffic never expires, and all purchases are final. Blocking images, as above, also cuts the GB a browser scraper uses. Scrape public data at a reasonable pace (is web scraping legal), then start with residential proxies and the Easy Trial: 1 GB for $2.95.

Web Scraping With Selenium and Python FAQ

How do I scrape a website with Selenium in Python while running Chrome in headless mode?

ProxyField residential proxies from $1.95/GB plug into this setup with one Chrome flag; the scraper itself is pip install selenium, then options.add_argument("--headless=new") on webdriver.ChromeOptions(), with webdriver.Chrome(options=options) as driver:, driver.get(url), a WebDriverWait(driver, 10) for the elements you need, and driver.find_elements(By.CSS_SELECTOR, ...) to read them. Set a window size such as 1366×900, because headless Chrome starts small. Selenium Manager downloads the matching chromedriver; if you see SessionNotCreatedException, Chrome and chromedriver differ in major version.

How do I use Selenium for web scraping?

You use Selenium for web scraping in 7 steps: install selenium, start headless Chrome, locate elements with By.CSS_SELECTOR or By.XPATH, wait with WebDriverWait, scroll or click for more items, take screenshots if needed, and parse driver.page_source into rows. Selenium 4.6 and newer download the browser driver by themselves.

Is Selenium good for web scraping?

Yes, for pages that need JavaScript, clicks or scrolling, and for teams that already know Selenium. For static HTML pages it is slow and heavy compared with Requests and Beautiful Soup, and for new browser scrapers Playwright is often quicker to write.

How do I install Selenium in Python?

Run pip install selenium inside a virtual environment; current releases need Python 3.10 or newer. Selenium Manager, included since 4.6, finds or downloads chromedriver or geckodriver when you first start a browser.

How do I locate elements in Selenium?

With driver.find_element(By.<strategy>, value) for the first match and driver.find_elements(...) for a list. CSS selectors cover most cases; use XPath to match text or move up the tree, and By.ID or By.NAME for form fields.

How do I wait for pages to load in Selenium?

Use an explicit wait: WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.CSS_SELECTOR, "..."))) waits for the element you need, up to 15 seconds. driver.get() already waits for the load event, but content added later by JavaScript needs the explicit wait.

What is the difference between implicit and explicit waits in Selenium?

An implicit wait is one global setting that makes every element lookup retry for up to N seconds; an explicit wait waits for one condition on one element. Selenium's documentation advises against mixing them, and explicit waits are the more precise choice.

Why does Selenium keep raising NoSuchElementException when I can see the element in my browser?

Because Selenium found no match at that exact moment, in that document. The usual causes, in order: the script looked before JavaScript added the element (use WebDriverWait(driver, 15).until(EC.presence_of_element_located(...))), the element is in an iframe (driver.switch_to.frame(...)), it is inside a shadow root (host.shadow_root.find_element(...)), a click opened another tab (driver.switch_to.window(...)), or the locator differs from what the headless page renders.

What is the difference between NoSuchElementException and StaleElementReferenceException?

NoSuchElementException means the element can't be found at that moment; StaleElementReferenceException means an element you already found can no longer be used, because the page refreshed, its DOM changed or you switched window or frame. Find the element again right before you use it.

Why does Selenium raise TimeoutException?

An explicit wait ran out before its condition came true, or a page took longer than the page load timeout, which is 300 seconds by default. Check that the condition can come true (the locator, the frame), then raise the timeout or use the eager page load strategy.

Show 5 more questionsShow fewer questions

How do I scrape infinite scroll pages with Selenium?

Scroll with driver.execute_script("window.scrollTo(0, document.body.scrollHeight);"), wait for new items, and repeat until the item count stops growing. Check the Network tab first, since the page usually loads the items from a JSON endpoint you can call directly.

How do I take screenshots in Selenium?

driver.save_screenshot("page.png") saves the visible viewport and element.screenshot("el.png") saves one element. For a full page, enlarge the window to the page height in Chrome, or use get_full_page_screenshot_as_file() with Firefox.

Why does headless Chrome crash in Docker with Selenium?

Containers often run Chrome as root without a sandbox and with a small /dev/shm, so Chrome exits at start. Add --no-sandbox and --disable-dev-shm-usage to the options, only inside the container, and keep --headless=new.

Why is Selenium scraping so slow?

Because every page loads in a full browser with scripts, styles and images, and every element lookup is a separate WebDriver call. Block images, use the eager page load strategy, reuse one driver, parse page_source once and run drivers in parallel.

Is Selenium good for crawling?

Only for small crawls of JavaScript pages: a browser per page is too slow and heavy for thousands of URLs. Crawl with Scrapy or an HTTP client and open Selenium only for the pages that need rendering.

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.