8 Best Web Scraping Tools for Python in 2026

04 September 2026 (updated) | 32 min read

The best web scraping tools for Python are built for very different jobs. Some help you parse HTML, some run a real browser, and others handle crawling, proxies, JavaScript rendering, or scraping infrastructure for you.

In this guide, we'll compare eight of the best Python web scraping tools for 2026: ScrapingBee, Playwright, Selenium, Scrapy, Crawlee, Beautiful Soup, selectolax, and curl_cffi. We'll cover what each tool is good at, where it falls short, and when it makes sense to use it.

We'll start with ScrapingBee, a managed web scraping API that takes care of much of the browser and proxy infrastructure for you. Then we'll move on to browser automation tools, crawling frameworks, and lightweight Python web scraping libraries for parsing and extracting data.

8 Best Web Scraping Tools for Python in 2026

Quick Answer (TL;DR)

  1. ScrapingBee — A managed web scraping API that handles proxies, JavaScript rendering, sessions, AI extraction, and other scraping infrastructure for you.

  2. Playwright — A great choice for Python web scraping when you need to run JavaScript, click buttons, fill forms, scroll pages, or work with other dynamic content.

  3. Selenium — A mature browser automation framework with broad browser support and a huge ecosystem. Useful when you need full browser control.

  4. Scrapy — A powerful crawling framework for large-scale web scraping, with built-in concurrency, request scheduling, pipelines, and data exports.

  5. Crawlee — A modern crawling framework that supports both lightweight HTTP scraping and full browser automation, along with queues, sessions, proxies, and adaptive rendering.

  6. Beautiful Soup — A simple and beginner-friendly library for parsing HTML and extracting data. You'll usually pair it with an HTTP client such as Requests.

  7. selectolax — A fast, lightweight HTML parser that works well when you need to process lots of pages using CSS selectors.

  8. curl_cffi — A newer HTTP client with browser impersonation and HTTP/2 and HTTP/3 support. Worth a look when regular Python HTTP clients are too easy to fingerprint.

Python Web Scraping Tools Comparison

Here's a quick comparison before we dive into each tool:

ToolCategoryBest forCostMain limitation
ScrapingBeeManaged scraping APIScraping without managing browsers, proxies, sessions, and related infrastructurePaid API; 1,000 free credits to startUsage costs money, especially for heavier rendering and proxy configurations
PlaywrightBrowser automationJavaScript-heavy sites and browser interactionsFree, open sourceFull browsers use more CPU and memory than direct HTTP requests
SeleniumBrowser automationBrowser-based scraping, broad browser support, and existing Selenium setupsFree, open sourceResource-heavy, and browser automation can still be detected
ScrapyCrawling frameworkLarge crawls, concurrency, pipelines, and structured exportsFree, open sourceMore setup and structure; no JavaScript rendering out of the box
CrawleeCrawling frameworkProjects that mix HTTP crawling with Playwright-based browser scrapingFree, open sourceYounger Python ecosystem than Scrapy
Beautiful SoupHTML parsing librarySimple, readable HTML extractionFree, open sourceDoes not fetch pages or execute JavaScript
selectolaxHTML parserFast, lightweight HTML parsing with CSS selectorsFree, open sourceParsing only; no JavaScript or full XPath support
curl_cffiHTTP clientLightweight HTTP scraping with browser-style network fingerprintsFree, open sourceNo JavaScript or HTML parsing; still classified as Beta

A note on cost: "Free" here means the tool itself is free and open source. You may still need to pay for servers, proxies, storage, or other infrastructure when running your own scrapers.

1. ScrapingBee Web Scraping API

ScrapingBee web scraping API homepage

ScrapingBee is a managed web scraping API that handles much of the infrastructure behind Python web scraping for you. Instead of running your own browser fleet or managing proxies yourself, your Python script sends a request to ScrapingBee and gets the page content or extracted data back.

The main HTML API supports JavaScript rendering, premium and stealth proxies, geotargeting, sessions and cookies, custom headers, screenshots, and JavaScript scenarios for actions such as clicking, scrolling, or waiting for elements. You can get the result as HTML, JSON, Markdown, or plain text, or extract specific fields with CSS or XPath rules.

ScrapingBee also includes several newer features that can save you some scraping logic:

  • AI extraction lets you describe what you want in natural language with ai_query, or define structured fields with ai_extract_rules.
  • Auto Mode tries different rendering and proxy configurations automatically and returns the first one that works. You can use max_cost to limit how expensive the selected configuration can be.
  • Dedicated APIs cover common targets and use cases, including Google Search, Fast Search, Amazon, YouTube, Walmart, ChatGPT, Gemini, and employee search.
  • Automation and AI integrations include a CLI, Remote MCP server, Make, n8n, Zapier, and LangChain.

ScrapingBee is a good fit when you need browser rendering, proxies, structured extraction, or access to harder-to-scrape pages but do not want to manage all of that infrastructure yourself.

Extract data with AI

The official Python SDK is the quickest way to get started. In this example, ai_query tells ScrapingBee what information to extract from a product page using plain English:

from scrapingbee import ScrapingBeeClient

client = ScrapingBeeClient(api_key="YOUR_API_KEY")

url = "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"

response = client.get(
    url,
    params={
        "ai_query": (
            "What book is this page selling? "
            "Give me the title, price, availability, "
            "and a one-sentence description of the book."
        ),
    },
)

response.raise_for_status()
print(response.text)

You do not have to use the SDK. ScrapingBee is an HTTP API, so you can call it with any Python HTTP client. The next example uses the third-party requests package together with Auto Mode and AI extraction:

import os

import requests

response = requests.get(
    "https://app.scrapingbee.com/api/v1/",
    params={
        "url": "https://news.ycombinator.com/",

        # Let ScrapingBee choose a working rendering
        # and proxy configuration.
        "mode": "auto",

        # Do not let Auto Mode select a configuration
        # that costs more than 25 credits.
        "max_cost": 25,

        # Extract the requested information using AI.
        "ai_query": (
            "Return the first five stories displayed on the homepage. "
            "For each story, include its title, URL, score, author, "
            "and number of comments."
        ),
    },
    headers={
        "Authorization": f"Bearer {os.environ['SCRAPINGBEE_API_KEY']}",
    },
    timeout=120,
)

response.raise_for_status()

print(
    "Auto Mode cost:",
    response.headers.get("Spb-auto-cost"),
    "credits",
)

print(
    "Total request cost:",
    response.headers.get("Spb-cost"),
    "credits",
)

print(response.text)

Auto Mode starts with cheaper configurations and moves to more expensive ones only when needed. max_cost limits the configuration Auto Mode can select, while separately billed features such as AI extraction are added to the total request cost.

ScrapingBee is a paid service rather than a local Python library, so cost is worth considering for large scraping jobs. New accounts get 1,000 free API credits to try the service. Join today!

Further reading

Want to explore some of ScrapingBee's newer features in more detail? Check out these guides:

2. Playwright

Playwright is an open-source browser automation library that lets Python scripts control Chromium, Firefox, and WebKit. It is widely used for end-to-end testing, but it is also a great tool for Python web scraping when you need to run JavaScript or interact with a page in a real browser.

Playwright can open pages, click buttons, fill forms, scroll, work with multiple tabs, intercept network requests, and extract data from the rendered DOM. Its locator API also includes auto-waiting, so you usually do not need to add fixed delays while waiting for dynamic elements to appear.

Playwright works well for:

  • JavaScript-heavy websites where the data is not available through a simpler HTTP request
  • pages that require clicks, scrolling, form submissions, or other browser interactions
  • infinite scroll and lazy-loaded content
  • workflows that depend on browser state, cookies, or authentication

The main trade-off is resource usage. Playwright runs real browser processes, so it needs more CPU and memory than lightweight HTTP clients and HTML parsers. It also does not provide managed proxy or anti-bot infrastructure out of the box.

Install the Python package and browser binaries with:

pip install playwright
playwright install

Here is a simple example using the JavaScript version of Quotes to Scrape. The quote cards are rendered with JavaScript, which makes this a good use case for browser automation:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

    page.goto("https://quotes.toscrape.com/js/")

    # Wait until JavaScript has rendered the quote cards.
    page.locator(".quote").first.wait_for()

    quotes = page.locator(".quote")

    for quote in quotes.all():
        text = quote.locator(".text").inner_text()
        author = quote.locator(".author").inner_text()
        tags = quote.locator(".tag").all_inner_texts()

        print({
            "text": text,
            "author": author,
            "tags": tags,
        })

    browser.close()

Here, Playwright runs the page's JavaScript and then extracts the rendered quote text, author, and tags. You can use the same approach to click pagination controls, handle infinite scrolling, submit forms, or wait for other dynamically loaded content.

Want to learn more? Check out our Playwright web scraping guide for practical tips on waiting for dynamic content, blocking unnecessary resources, running pages in parallel, and scraping JavaScript-heavy websites.

3. Selenium

Selenium is a mature browser automation framework that lets Python scripts control browsers such as Chrome, Firefox, Edge, and Safari. It was originally built for testing web applications, but it is also widely used for Python web scraping when a page requires JavaScript or browser interaction.

With Selenium, you can open pages, click buttons, fill forms, scroll, work with cookies and sessions, switch between tabs or frames, and extract content after the browser renders it. For dynamic pages, Selenium provides explicit waits through WebDriverWait, so you can wait for a specific element or condition instead of relying on fixed sleep() calls.

Selenium works well when you need:

  • JavaScript rendering and access to the rendered DOM
  • clicks, form submissions, scrolling, or other browser interactions
  • support for a wide range of browsers
  • an established browser automation ecosystem or existing Selenium setup
  • distributed browser execution with Selenium Grid

The main drawback is resource usage. Selenium runs a full browser, so it needs more CPU and memory than plain HTTP requests. Large scraping jobs also require extra infrastructure if you want to run many browser instances at the same time.

Modern Selenium includes Selenium Manager, which handles browser drivers automatically in most common setups. In most cases, you no longer need to download ChromeDriver manually or install a third-party package such as webdriver-manager.

Install Selenium with:

pip install selenium

Here is an example using the JavaScript version of Quotes to Scrape. Selenium runs Chrome in headless mode, waits for the quote cards to appear, and then extracts their text, author, and tags:

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

options = webdriver.ChromeOptions()
options.add_argument("--headless")

driver = webdriver.Chrome(options=options)

try:
    driver.get("https://quotes.toscrape.com/js/")

    # Wait until JavaScript has rendered the quote cards.
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CLASS_NAME, "quote"))
    )

    quotes = driver.find_elements(By.CLASS_NAME, "quote")

    for quote in quotes:
        text = quote.find_element(By.CLASS_NAME, "text").text
        author = quote.find_element(By.CLASS_NAME, "author").text
        tags = [
            tag.text
            for tag in quote.find_elements(By.CLASS_NAME, "tag")
        ]

        print({
            "text": text,
            "author": author,
            "tags": tags,
        })

finally:
    driver.quit()

Unlike a simple HTTP client, Selenium runs the page's JavaScript before extracting the data. Explicit waits are useful on dynamic sites because they let you wait for the content you actually need instead of guessing how long the page might take to load.

Want to learn more? Read our guide to web scraping with Selenium and Python for browser setup, dynamic content, waits, proxies, scaling, and more advanced scraping techniques.

4. Scrapy

Scrapy is a mature Python framework built for web crawling and structured data extraction. Instead of handling pages one by one, Scrapy gives you the tools to build complete crawling workflows with request scheduling, concurrency, retries, data pipelines, and exports.

That makes Scrapy a strong choice for large-scale web scraping when you need to crawl many pages rather than a handful of individual URLs. Its main features include:

  • asynchronous requests and configurable concurrency
  • spiders for defining crawling and extraction logic
  • CSS selectors and XPath
  • item pipelines for cleaning, transforming, and storing data
  • downloader and spider middleware for customizing requests and responses
  • built-in exports to JSON, JSON Lines, CSV, and XML
  • AutoThrottle and other controls for managing crawl speed
  • support for pausing and resuming long-running crawls

Scrapy takes a bit more setup than a simple Requests or Beautiful Soup script, so there is a learning curve. It also does not run a browser or render JavaScript on its own. If a page needs browser rendering, you can combine Scrapy with Playwright through scrapy-playwright, or send those requests through a scraping API.

Install Scrapy with:

pip install scrapy

The following spider crawls Quotes to Scrape, extracts each quote, and follows the pagination until there are no more pages:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = ["https://quotes.toscrape.com/"]

    def parse(self, response):
        for quote in response.css(".quote"):
            yield {
                "text": quote.css(".text::text").get(),
                "author": quote.css(".author::text").get(),
                "tags": quote.css(".tag::text").getall(),
            }

        next_page = response.css("li.next a::attr(href)").get()

        if next_page:
            yield response.follow(next_page, callback=self.parse)

Save the spider as quotes_spider.py, then run it and export the results to JSON:

scrapy runspider quotes_spider.py -O quotes.json

Scrapy takes care of scheduling requests and following the pagination, while the spider defines which pages to follow and what data to extract. That becomes especially useful when a Python web scraping project grows from a few pages to thousands or more.

Want to learn more? Read our web scraping with Scrapy guide for more on spiders, selectors, pagination, exports, and building larger Scrapy projects.

5. Crawlee

Crawlee is a modern open-source crawling framework for Python. It supports both lightweight HTTP scraping and full browser automation, so you can use the same framework for simple pages and JavaScript-heavy websites.

Crawlee comes with several crawler types. BeautifulSoupCrawler and ParselCrawler use regular HTTP requests, while PlaywrightCrawler launches a real browser when JavaScript rendering or browser interaction is needed.

For larger Python web scraping projects, Crawlee also includes:

  • automatic concurrency and resource-based autoscaling
  • persistent request queues
  • retries and error handling
  • session and cookie management
  • proxy management
  • built-in data storage
  • request routing and link discovery
  • similar APIs for HTTP and browser-based crawling

One of its more interesting features is AdaptivePlaywrightCrawler. It can use lightweight HTTP crawling where possible and fall back to Playwright when browser rendering is needed. This can save CPU and memory compared with launching a browser for every page.

Crawlee is much newer than Scrapy and has a smaller Python ecosystem. On the other hand, its built-in Playwright support and its mix of HTTP and browser crawling make it a useful option for new scraping projects.

For this example, we'll use BeautifulSoupCrawler to scrape quotes, follow the site's pagination, and save everything to a JSON file:

pip install "crawlee[beautifulsoup]"
import asyncio

from crawlee.crawlers import (
    BeautifulSoupCrawler,
    BeautifulSoupCrawlingContext,
)


async def main() -> None:
    crawler = BeautifulSoupCrawler()

    @crawler.router.default_handler
    async def request_handler(
        context: BeautifulSoupCrawlingContext,
    ) -> None:
        for quote in context.soup.select(".quote"):
            await context.push_data({
                "text": quote.select_one(".text").get_text(strip=True),
                "author": quote.select_one(".author").get_text(strip=True),
                "tags": [
                    tag.get_text(strip=True)
                    for tag in quote.select(".tag")
                ],
            })

        # Add the next pagination link to the request queue.
        await context.enqueue_links(selector="li.next a")

    await crawler.run(["https://quotes.toscrape.com/"])

    # Export all collected items to a single JSON file.
    await crawler.export_data("quotes.json")


if __name__ == "__main__":
    asyncio.run(main())

The crawler starts with the first page, extracts the quotes, and adds the Next link to its request queue. It keeps doing this until there are no more pages left.

Each extracted quote is stored in Crawlee's default dataset. When the crawl finishes, export_data() writes the complete dataset to quotes.json.

If a target needs JavaScript rendering, you can use the same general workflow with PlaywrightCrawler instead.

Want to learn more? Check out our Crawlee for Python tutorial with examples for more on HTTP crawling, Playwright integration, concurrency, proxies, and storing scraped data.

6. Beautiful Soup

Beautiful Soup is one of the most popular Python libraries for extracting data from HTML and XML. It gives you a simple API for navigating the document tree, finding elements, and selecting content with CSS selectors.

Beautiful Soup does not fetch web pages or run a browser. You normally pair it with an HTTP client such as Requests: Requests downloads the HTML, and Beautiful Soup parses it and extracts the data you need.

It can use several parser backends, including Python's built-in html.parser, html5lib, and lxml. Using lxml is a common choice when you want faster parsing while keeping Beautiful Soup's simple API.

Install Requests, Beautiful Soup, and lxml with:

pip install requests beautifulsoup4 lxml

Here's a simple Python web scraping example that extracts the current stories from Hacker News:

import requests
from bs4 import BeautifulSoup

response = requests.get(
    "https://news.ycombinator.com/",
    timeout=30,
)
response.raise_for_status()

soup = BeautifulSoup(response.text, "lxml")

for story in soup.select("tr.athing"):
    link = story.select_one(".titleline > a")

    print({
        "title": link.get_text(strip=True),
        "url": link.get("href"),
    })

Requests fetches the page, while Beautiful Soup handles the HTML parsing and extraction. Passing "lxml" tells Beautiful Soup to use lxml as its parser backend.

Beautiful Soup supports CSS selectors through Soup Sieve, but it does not support XPath. If you need XPath, you can use lxml directly instead:

import requests
from lxml import html

response = requests.get(
    "https://news.ycombinator.com/",
    timeout=30,
)
response.raise_for_status()

tree = html.fromstring(response.content)

titles = tree.xpath(
    '//span[@class="titleline"]/a/text()'
)

for title in titles:
    print(title)

Beautiful Soup is a great fit when you already have the HTML and want a simple, readable way to extract data from it. If parsing speed matters more, or you need XPath, lxml or selectolax may be a better choice.

Want to learn more? Read our Beautiful Soup web scraping tutorial for more on Requests, CSS selectors, HTML parsing, and structured data extraction.

7. selectolax

selectolax is a lightweight HTML parser built for speed. It uses Cython and native parsing engines, which lets performance-critical parts run as compiled code instead of regular Python. In practice, that makes selectolax a good fit when you need to parse a large number of HTML documents quickly.

Like Beautiful Soup, selectolax focuses on parsing rather than fetching pages. You normally pair it with an HTTP client such as Requests. Once you have the HTML, selectolax gives you a compact API for navigating the document tree and extracting elements with CSS selectors.

Its main advantages include:

  • fast HTML parsing with the Lexbor backend
  • CSS selector support
  • a small and straightforward API
  • DOM traversal and manipulation
  • support for both complete HTML documents and fragments

selectolax does not run JavaScript or handle requests, sessions, retries, or crawling workflows. It also does not provide full XPath support. If you need XPath or extensive XML processing, lxml is usually a better choice.

Install selectolax together with Requests:

pip install selectolax requests

The following Python web scraping example downloads the first page of Books to Scrape and extracts the title, price, and availability of every book:

import requests
from selectolax.lexbor import LexborHTMLParser

response = requests.get(
    "https://books.toscrape.com/",
    timeout=30,
)
response.raise_for_status()

tree = LexborHTMLParser(response.content)

for book in tree.css("article.product_pod"):
    link = book.css_first("h3 a")
    price = book.css_first(".price_color")
    availability = book.css_first(".availability")

    print({
        "title": link.attributes["title"],
        "price": price.text(strip=True),
        "availability": availability.text(strip=True),
    })

Requests handles the HTTP request, while selectolax parses the returned HTML with its Lexbor backend. The css() method returns all matching nodes, while css_first() returns the first match inside each book card.

selectolax is a good choice when parsing speed matters and you do not need the broader convenience API of Beautiful Soup or the crawling features of Scrapy and Crawlee.

8. curl_cffi

curl_cffi is a Python HTTP client based on a browser-impersonating fork of curl. Its API feels similar to Requests, but it adds one feature that makes it especially interesting for modern web scraping: browser impersonation at the TLS and HTTP level.

A regular Python HTTP client does not connect to a website in exactly the same way as Chrome, Firefox, or Safari. Changing the User-Agent header alone is not enough, because servers can also look at details of the TLS handshake and HTTP connection to identify automated clients.

curl_cffi can make those network-level fingerprints look much closer to a real browser:

from curl_cffi import requests

response = requests.get(
    "https://example.com/",
    impersonate="chrome",
)

With impersonate="chrome", curl_cffi uses a Chrome-like TLS and HTTP fingerprint instead of only changing a request header.

Other useful features include:

  • a Requests-like API
  • synchronous and asynchronous clients
  • HTTP/2 and HTTP/3 support
  • sessions, cookies, and proxies
  • WebSocket support
  • browser impersonation for Chrome, Firefox, Safari, and other supported targets
  • custom TLS and HTTP fingerprints for advanced use cases

This makes curl_cffi useful when you want the speed and low overhead of direct HTTP requests but need a more realistic browser-like network fingerprint than a regular Python HTTP client can provide.

It is still just an HTTP client, though. curl_cffi does not run JavaScript or let you click buttons, fill forms, or interact with a page like Playwright or Selenium. Browser impersonation also does not guarantee that every anti-bot system will accept the request. Sites can look at IP reputation, cookies, JavaScript behavior, request frequency, and many other signals.

You also need a separate HTML parser to extract data from the response. For example, you can combine curl_cffi with selectolax:

pip install curl-cffi selectolax
from curl_cffi import requests
from selectolax.lexbor import LexborHTMLParser

response = requests.get(
    "https://books.toscrape.com/",
    impersonate="chrome",
    timeout=30,
)
response.raise_for_status()

tree = LexborHTMLParser(response.content)

for book in tree.css("article.product_pod"):
    link = book.css_first("h3 a")
    price = book.css_first(".price_color")

    print({
        "title": link.attributes["title"],
        "price": price.text(strip=True),
    })

Here, curl_cffi handles the HTTP request and browser-style network fingerprint, while selectolax parses the HTML and extracts the data.

curl_cffi is newer than most of the Python web scraping tools in this list and is currently classified as Beta on PyPI. It is actively developed and worth checking out for advanced HTTP-based scraping, but for critical production projects, test it carefully against the sites and environments you actually plan to use.

Want to learn more? Read our guide to using curl_cffi for web scraping in Python for more on TLS fingerprinting, browser impersonation, async requests, proxies, and when you may need to move from a lightweight HTTP client to a full browser.

Data Extraction Techniques

Once you have retrieved a page, the next step is to extract the data you actually need. In Python web scraping, the best method depends on how the site exposes that data.

CSS selectors

CSS selectors are usually the easiest way to find elements in HTML. They are supported by Beautiful Soup, selectolax, Scrapy, Playwright, and many other Python scraping tools.

For example:

title = soup.select_one("article.product h2").get_text(strip=True)

Try to keep selectors as simple and stable as possible. A selector like this:

.product-card .price

is usually a better choice than a long chain such as:

main > div:nth-child(3) > div > div:nth-child(2) > span

Long selectors depend too much on the exact page layout and can break after a small redesign.

Also be careful with class names that look auto-generated, such as:

css-1x7a9k3
sc-bdfBwQ
jsx-194882391

These may change between builds. If the page provides a stable ID, semantic class, or data-* attribute, that can be a better target:

[data-product-id="123"]
[data-testid="product-price"]

Just remember that data-testid and similar attributes are usually meant for the site's own tests, not as a public API, so they can still change.

XPath

XPath is useful when you need more flexible queries over the document tree. Scrapy and lxml support XPath directly:

title = tree.xpath("//article[@class='product']//h2/text()")

The same stability rule applies here: avoid absolute XPath expressions copied directly from browser DevTools when possible.

Something like:

/html/body/div[2]/main/div[3]/div[1]/span

is very easy to break. A shorter expression based on meaningful attributes is usually safer:

//article[@data-product-id]//span[@class="price"]/text()

You can also locate elements by their text when there is no better hook:

//button[contains(normalize-space(), "Load more")]

This can be useful, but text itself can change or be translated on localized versions of the site, so treat it as a fallback rather than your first choice.

Structured and embedded data

Before writing complex selectors, check whether the page already includes structured data. Many sites expose JSON-LD, application state, or other JSON inside <script> elements.

For example, a product page might include schema.org data like this:

<script type="application/ld+json">
{
  "@type": "Product",
  "name": "Example Product",
  "offers": {
    "price": "29.99"
  }
}
</script>

Data like this is often easier to extract than the visible HTML. It can also be more stable because it describes the actual product or page data rather than the current visual layout.

Still, validate what you find. JSON-LD may contain several objects, outdated values, or only part of the information shown on the page.

Network requests and APIs

On JavaScript-heavy pages, the data you need may come from an API request rather than the rendered HTML.

You can often find these requests in your browser's developer tools:

  1. Open the Network tab.
  2. Reload the page.
  3. Filter for Fetch/XHR requests.
  4. Perform the action that loads the data, such as changing a page, applying a filter, or scrolling.
  5. Inspect responses that return JSON or other structured data.

If you find the endpoint that supplies the data, calling it directly from Python is usually faster and uses fewer resources than running Selenium or Playwright.

Do not assume that every endpoint will be easy to reuse, though. Some require cookies, authentication tokens, CSRF headers, signed parameters, or other session data. Internal APIs can also change without warning because they are built for the website itself rather than external users.

Developer tools can help here as well: copying a working request as cURL is often a useful starting point for figuring out which headers, parameters, and cookies actually matter.

Attributes and text

Useful data is not always visible text. URLs, image sources, IDs, timestamps, and other values are often stored in HTML attributes:

url = link.get("href")
image = img.get("src")

Sometimes the machine-readable value is actually better than the visible one. A page might display:

<time datetime="2026-08-31T12:30:00Z">
    August 31
</time>

In that case, extracting datetime is usually safer than trying to parse the formatted text.

Also watch for relative URLs:

<a href="/products/123">

You may need to resolve them against the site's base URL before storing them.

Pattern extraction

Regular expressions are useful for finding patterns inside text, such as phone numbers, dates, SKUs, product IDs, or other consistently formatted values.

For example:

import re

product_id = re.search(r"SKU:\s*(\w+)", text)

Regex works best after an HTML parser has already narrowed the content down to the relevant text. Trying to parse an entire HTML document with regular expressions is usually fragile and unnecessary.

Normalize and validate extracted data

Raw scraped data often needs some cleanup before you can use it. Common steps include:

  • trimming whitespace
  • converting prices and numbers to consistent formats
  • resolving relative URLs
  • parsing dates
  • removing duplicate records
  • checking that required fields are present

Validation also matters in production. A 200 OK response only tells you that the request succeeded; it does not mean your scraper extracted the right data.

Watch for missing fields, empty results, unexpected value formats, or sudden changes in record counts. These are often the first signs that a selector, API, or page structure has changed.

For important scrapers, it is worth adding a few basic assumptions to your code. If every product should have a title and price, for example, treat a page that suddenly returns 200 products with no prices as an error rather than quietly saving bad data.

Further reading

For a deeper look at HTML extraction and selector strategies, check out these ScrapingBee guides:

Scraping Dynamic Websites

Scraping dynamic websites is a bit different from scraping plain HTML pages. Some content may appear only after JavaScript runs, an API request finishes, the user scrolls, or the page updates its own state.

But dynamic does not automatically mean you need a browser.

Check for API requests first

Before reaching for Playwright or Selenium, check the site's network activity. Many dynamic pages simply fetch their data from JSON, GraphQL, or other API endpoints in the background.

If you can call that endpoint directly, it is usually faster, cheaper, and easier to scale than running a full browser.

Also check the initial HTML for embedded JSON, JSON-LD, or serialized application state. Sometimes the data you need is already there even if the page later renders it with JavaScript.

Use a browser when JavaScript is actually required

If the data only appears after JavaScript runs or the page needs real browser interaction, tools such as Playwright or Selenium make sense.

Typical cases include:

  • clicking buttons or opening menus
  • submitting forms
  • waiting for client-side rendering
  • infinite scrolling or lazy loading
  • authenticated browser sessions
  • content that depends on browser state

When scraping dynamic pages, wait for the content you actually need instead of using fixed delays such as sleep(5). Playwright provides auto-waiting through its locator API, while Selenium supports explicit waits with WebDriverWait.

Use browser rendering only where you need it

For larger crawls, sending every page through a full browser can waste a lot of CPU and memory.

With Scrapy, you can use scrapy-playwright to render only the pages that actually need JavaScript while keeping the rest of the crawl on lightweight HTTP requests.

Crawlee takes a similar approach with separate HTTP and Playwright-based crawlers. Its AdaptivePlaywrightCrawler can even switch between regular HTTP fetching and browser rendering depending on whether the page can be scraped without JavaScript.

Use a managed scraping API when you do not want to run browsers yourself

Running many browser instances also means dealing with CPU and memory usage, proxies, sessions, retries, and browser infrastructure.

A managed API such as ScrapingBee handles JavaScript rendering and proxy infrastructure remotely. Your Python scraper sends a request and gets the rendered page or extracted data back without running its own browser fleet.

A good rule of thumb is:

  1. Use a direct HTTP request if the data is already available.
  2. Call the underlying API or extract embedded data if the page loads it dynamically.
  3. Use Playwright or Selenium when you really need JavaScript or browser interaction.
  4. Use Scrapy or Crawlee with selective browser rendering for larger crawls.
  5. Use a managed scraping API when you want to offload browser and proxy infrastructure.

Common Web Scraping Challenges

Most Python web scraping projects eventually run into at least a few of these problems:

  • JavaScript-rendered content — the data may not be present in the initial HTML. Check for an underlying API first, then use Playwright, Selenium, Crawlee, or a managed rendering API if you really need a browser.
  • Rate limits and 429 responses — slow down your requests, limit concurrency, respect hints such as Retry-After, and use backoff instead of retrying immediately.
  • IP-based blocking — larger crawls may need proxy rotation, geotargeting, or better request distribution.
  • CAPTCHAs and challenge pages — changing IPs alone may not solve these. Modern anti-bot systems can also look at browser fingerprints, JavaScript behavior, cookies, IP reputation, and request patterns.
  • Changing page structure — selectors, URLs, pagination, APIs, and layouts can all change. Monitor important fields and record counts so broken scrapers do not fail silently.
  • Authentication and session state — some sites require cookies, login flows, CSRF tokens, or persistent sessions.
  • Pagination, infinite scroll, and lazy loading — make sure your scraper discovers every page or triggers additional content when needed.
  • Data quality — a successful HTTP request does not mean the right data was extracted. Validate required fields, normalize values, resolve relative URLs, and watch for duplicates or incomplete records.
  • Performance and resource usage — browser automation uses much more CPU and memory than direct HTTP requests. Avoid Playwright or Selenium when plain HTML or an API gives you the same data.
  • Scaling infrastructure — retries, queues, concurrency, browser instances, proxies, storage, and monitoring become more important as your scraper grows.
  • Legal and contractual requirements — check the target site's terms of service and any laws or contractual rules that apply to your use case.

Choosing the Right Tool

There is no single Python web scraping tool that works best for every project. What makes sense depends on whether you need managed scraping infrastructure, a real browser, large-scale crawling, or just fast HTML parsing.

  • ScrapingBee works well when you want a managed API to handle browser rendering, proxies, geotargeting, sessions, and other scraping infrastructure. It can save you from maintaining your own browser and proxy stack.
  • Playwright or Selenium make sense when the target really needs JavaScript execution or browser interaction, such as clicking elements, submitting forms, scrolling, or maintaining browser state.
  • Scrapy is a strong fit for large crawls where request scheduling, concurrency, retries, pipelines, middleware, and structured exports matter.
  • Crawlee is worth considering if you want a newer crawling framework that combines regular HTTP crawling with Playwright, plus sessions, proxies, storage, and adaptive browser rendering.
  • Beautiful Soup is a simple option when you already have the HTML and mainly need a readable API for finding and extracting data.
  • selectolax is useful when parsing speed matters and CSS selectors give you everything you need.
  • curl_cffi fits more advanced HTTP-based scraping where you want lightweight requests but need more realistic TLS and HTTP fingerprints than a regular Python HTTP client provides. It is still a newer project and currently classified as Beta.

Playwright or Selenium?

Let's touch on a browser automation debate that has been going on for years: Playwright vs. Selenium.

People love arguing about which one is faster, cleaner, more modern, or more reliable. In practice, though, the answer is much less dramatic: both are good, and the better choice usually depends on your project and which API you prefer working with.

Playwright often feels like the newer and more streamlined option, especially thanks to features such as auto-waiting and its locator API. But that does not automatically make it faster or more efficient than Selenium in every case.

A 2025 empirical study comparing Playwright, Selenium, and Cypress tested ten browser automation scenarios, running each one ten times and measuring execution time, CPU usage, and memory use. Playwright was faster than Selenium in six scenarios, while Selenium won the other four. Playwright generally used less CPU, while Selenium used less RAM in six out of ten tests.

These were browser automation tests rather than web scraping benchmarks, so the numbers do not tell us which tool will scrape every site faster. They do show that the simple idea of "Playwright is newer, so it must be faster and better" does not really hold up.

For a new project, Playwright is often a nice place to start because of its built-in waiting behavior and modern API. Selenium is still actively developed, has broad browser support, Selenium Grid, and a huge existing ecosystem.

So unless your project has a specific requirement that clearly favors one of them, use the one you like working with more. If performance really matters, benchmark both on the pages and interactions your scraper will actually use.

And if a normal HTTP request, embedded data, or an underlying API gives you the same result, skip the debate entirely and do not launch a browser in the first place.

Conclusion

Python web scraping in 2026 gives you a lot of good options. Beautiful Soup and selectolax work well for straightforward HTML parsing, Scrapy and Crawlee are better for larger crawls, and Playwright or Selenium make sense when you really need a browser. curl_cffi is also worth a look if you want to stay with lightweight HTTP requests but need more advanced browser-style fingerprinting.

The main rule is simple: do not use more infrastructure than the job needs. If a regular HTTP request or an underlying API gives you the data, use that. If the site really needs JavaScript or browser interaction, move to browser automation. And if running browsers, proxies, sessions, retries, and other scraping infrastructure starts becoming a job of its own, a managed scraping API can take a lot of that work off your hands.

ScrapingBee handles browser rendering, proxy rotation, geotargeting, extraction, and other scraping infrastructure through a single API. It also includes newer features such as Auto Mode and AI-powered extraction.

Ready to try it? Create a free ScrapingBee account and get 1,000 free API credits to test it on your own scraping projects. No credit card is required.

Python Web Scraping Tools FAQ

What is the best Python web scraping tool?

There is no single best web scraping tool for every Python project. Beautiful Soup and selectolax are great for HTML parsing, Scrapy and Crawlee work well for larger crawls, and Playwright or Selenium make sense when you need a real browser. ScrapingBee is a strong option when you want a managed API to handle browser rendering, proxies, sessions, and other scraping infrastructure for you.

When should I use a managed web scraping API?

A managed web scraping API is useful when proxies, browser rendering, sessions, retries, geotargeting, or anti-bot challenges start taking too much time to manage yourself. It is also a good fit when you want to scale scraping without running your own browser fleet or proxy infrastructure.

If you want to compare ScrapingBee with other managed and hosted scraping solutions, check out our 17 best web scraping tools for 2026 roundup.

When is a DIY Python scraper the better choice?

A DIY scraper makes sense when the target is simple, the data is available through regular HTTP requests, and you want full control over the scraping logic and infrastructure. For many static sites, an HTTP client plus Beautiful Soup, lxml, or selectolax may be all you need.

If you want to build one from scratch, our Python web scraping tutorial for 2026 walks through the whole process with practical examples, from simple HTTP requests and HTML parsing to browser automation and managed scraping APIs.

Which Python web scraping tools support JavaScript?

Playwright and Selenium run real browsers and can execute JavaScript directly. Crawlee can use Playwright through its browser-based crawlers, while Scrapy can be combined with scrapy-playwright. ScrapingBee can also render JavaScript remotely through its API.

Beautiful Soup, selectolax, and curl_cffi do not execute JavaScript on their own.

If you want to dig deeper into the different approaches, check out our guide to scraping JavaScript-heavy websites with Python. It covers direct API requests, browser automation, and managed JavaScript rendering.

Which Python scraping tools support browser impersonation?

curl_cffi can imitate browser-style TLS and HTTP fingerprints while still making lightweight HTTP requests.

Playwright and Selenium use real browsers, but browser automation can still leave signals that anti-bot systems detect. Third-party tools can patch some of those signals. For Playwright, projects such as playwright-stealth apply browser fingerprint tweaks and other stealth patches. For Selenium, undetected_chromedriver has long been a popular option, although newer tools such as Nodriver are worth looking at as well.

None of these tools makes browser automation truly "undetectable." They can reduce some common automation signals, but advanced anti-bot systems also look at IP reputation, TLS fingerprints, cookies, JavaScript behavior, request patterns, and other signals.

ScrapingBee provides managed proxy and browser infrastructure, including premium and stealth proxy options, so you do not have to build and maintain that layer yourself.

For more background, check out our Playwright web scraping guide and browser fingerprinting comparison with CreepJS.

Do I always need a browser to scrape dynamic websites?

No. Many dynamic websites load their data from JSON, GraphQL, or other API endpoints in the background. If you can call that endpoint directly, it is usually faster, cheaper, and easier to scale than running a browser.

Use Playwright, Selenium, or another browser-based solution when the data really depends on JavaScript execution, user interaction, or browser state.

For a deeper look at the different options, see our guide to scraping dynamic content with Python, which covers browser rendering, APIs, and other ways to handle JavaScript-heavy pages.

image description
Ilya Krukowski

Ilya is an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, tweets, goes in for sports and plays music.

New: Scrape any product from Shopee Indonesia

Try Shopee API Now