Web Scraping with Python

Updated June 2026
Python is the most widely used language for web scraping, offering a rich ecosystem of libraries that handle everything from simple HTTP requests to full browser automation. This guide covers the core libraries, techniques, and best practices you need to build reliable scrapers for both static and JavaScript-rendered websites.

What Is Web Scraping

Web scraping is the automated process of extracting data from websites. Instead of manually copying information from a browser, a scraper sends HTTP requests to web pages, downloads the HTML response, and parses it to pull out specific data points like prices, titles, contact details, or any other structured content embedded in the page.

At a high level, every web scraper follows the same three-step cycle. First, it fetches a page by sending an HTTP request to a URL. Second, it parses the returned HTML to locate the elements that contain the target data. Third, it extracts those values and stores them in a structured format such as a CSV file, a database row, or a JSON object. Some scrapers add a fourth step, following links on the page to discover new URLs and repeating the cycle across hundreds or thousands of pages.

Web scraping serves a wide range of use cases. Researchers use it to collect datasets for academic studies. Businesses use it for price monitoring, lead generation, and competitive analysis. Journalists use it to aggregate public records. Data scientists use it to build training sets for machine learning models. In each case, scraping replaces hours of manual data collection with a script that runs in seconds or minutes.

The complexity of a scraping task depends on the target website. Some sites serve plain HTML that is easy to parse. Others rely heavily on JavaScript to render content, require authentication, implement anti-bot protections, or paginate results across dozens of pages. Python has libraries that address every one of these challenges, which is why it has become the standard language for scraping work.

Why Python Dominates Web Scraping

Python accounts for the majority of web scraping projects worldwide, and its dominance in this space comes down to several reinforcing advantages.

The first advantage is readability. Python's syntax is clean and concise, which means scraper code is easy to write, easy to read, and easy to hand off to another developer. A complete scraper in Python often fits in under 50 lines, while the equivalent in Java or C# would require significantly more boilerplate. This low barrier to entry means that even people who are not full-time developers, such as analysts, researchers, and marketers, can write functional scrapers after a short learning curve.

The second advantage is the library ecosystem. Python has more mature, actively maintained scraping libraries than any other language. The Requests library alone is downloaded over 50 million times per week from PyPI. BeautifulSoup, Scrapy, Playwright, Selenium, lxml, httpx, and dozens of smaller tools each solve a specific part of the scraping problem. Whatever challenge you encounter, from JavaScript rendering to CAPTCHA handling to proxy rotation, there is a Python package designed for it.

The third advantage is integration with the data science stack. After you scrape data, you typically need to clean, transform, and analyze it. Python's pandas library is the industry standard for tabular data manipulation, and it integrates seamlessly with scraping workflows. You can go from raw HTML to a cleaned DataFrame to a visualization or a machine learning model without leaving Python. This end-to-end capability is something no other language matches as smoothly.

The fourth advantage is community support. Python's popularity means that nearly every scraping question has already been asked and answered on Stack Overflow, in blog posts, or in library documentation. When a website changes its layout and breaks your scraper, you can usually find someone who has encountered the same problem and posted a solution. This community knowledge base dramatically reduces the time spent debugging.

Finally, Python runs on every major operating system and integrates easily with scheduling tools like cron, Airflow, and Prefect, cloud platforms like AWS Lambda and Google Cloud Functions, and containerization tools like Docker. This flexibility makes it straightforward to move a scraper from a development laptop to a production environment.

Core Python Libraries for Web Scraping

Python's scraping ecosystem is built in layers. HTTP clients fetch pages, parsers extract data from HTML, and frameworks combine both into complete crawling systems. Understanding what each library does helps you choose the right tool for each project.

Requests

Requests is Python's most popular HTTP library, used by the vast majority of scraping projects for fetching web pages. It provides a simple, intuitive API for sending GET and POST requests, handling cookies, managing sessions, and setting custom headers. Requests handles redirects, SSL verification, and connection pooling automatically. Its main limitation is that it only retrieves the raw HTML response from the server, which means it cannot execute JavaScript or interact with dynamic page elements. For static websites, Requests is all you need on the fetching side.

BeautifulSoup

BeautifulSoup (imported as bs4) is a parsing library that converts raw HTML into a navigable tree structure. You can search for elements by tag name, CSS class, ID, attribute value, or CSS selector. BeautifulSoup is tolerant of malformed HTML, which is common on real-world websites, and it provides a forgiving, Pythonic API that is easy to learn. It does not fetch pages on its own, so it is almost always paired with Requests. The combination of Requests and BeautifulSoup is the classic starting point for Python web scraping and remains the right choice for straightforward projects that target static HTML content.

Scrapy

Scrapy is a full-featured web crawling framework rather than a single library. It includes an asynchronous request engine that handles concurrency automatically, middleware hooks for proxy rotation and user-agent randomization, data pipelines for cleaning and exporting results, and a built-in crawler that follows links across an entire site. Scrapy is the standard choice for large-scale scraping projects where you need to crawl thousands or millions of pages with structured output. It has a steeper learning curve than Requests plus BeautifulSoup, but the payoff is a production-ready system that handles scheduling, retries, rate limiting, and data export out of the box.

Playwright

Playwright is Microsoft's browser automation library, and it has become the preferred tool for scraping JavaScript-heavy websites. It controls real Chromium, Firefox, and WebKit browser instances, rendering pages exactly as a human user would see them. Playwright supports headless mode for faster execution, handles single-page applications built with React, Vue, or Angular, and includes built-in waiting mechanisms that pause execution until specific elements appear on the page. Its API is cleaner and more reliable than Selenium's, and it offers better performance for most scraping tasks. Playwright can also intercept network requests, which lets you capture API responses directly rather than parsing rendered HTML.

Selenium

Selenium was the original browser automation tool for Python and remains widely used in legacy systems. It drives real browsers through the WebDriver protocol and supports Chrome, Firefox, Edge, and Safari. For new scraping projects, Playwright is generally the better choice due to its faster execution, more reliable element waiting, and simpler API. However, Selenium still makes sense for teams with existing Selenium infrastructure, for projects that need Safari support, or for organizations that have invested in Selenium Grid for distributed browser testing.

httpx

httpx is a modern HTTP client that is API-compatible with Requests but adds native async/await support, HTTP/2 capability, and improved connection pooling. If you need to make many concurrent requests to different pages, httpx's async mode can dramatically improve throughput compared to sequential Requests calls. It is a good choice for high-performance static scraping where you want to fetch hundreds of pages concurrently without the overhead of a full framework like Scrapy.

lxml

lxml is a high-performance XML and HTML parser built on the C libraries libxml2 and libxslt. It parses HTML significantly faster than BeautifulSoup, which matters when you are processing large documents or high volumes of pages. lxml supports both CSS selectors and XPath expressions, with XPath being particularly powerful for complex element selection patterns. The trade-off is that lxml is less forgiving of malformed HTML than BeautifulSoup and has a slightly steeper learning curve.

Scrapling

Scrapling is a newer library that has gained attention in 2025 and 2026 for its Adaptive Parsing feature. Traditional scrapers break when a website changes its HTML structure, requiring manual updates to selectors. Scrapling's adaptive parser can automatically adjust to structural changes, reducing maintenance overhead for long-running scraping projects. It also includes built-in anti-detection features that help avoid bot-blocking mechanisms. While not as established as the libraries above, Scrapling is worth evaluating for projects where scraper maintenance is a significant concern.

Static vs Dynamic Page Scraping

The single most important decision in any scraping project is whether the target pages are static or dynamic, because this determines which tools you need.

Static pages deliver all their content in the initial HTML response from the server. When you send a GET request and examine the HTML, every piece of data you want is already there in the markup. Most news articles, blog posts, documentation sites, government data portals, and Wikipedia pages are static. For these sites, Requests plus BeautifulSoup or lxml is the fastest and most efficient approach. There is no need to launch a browser, which saves memory, CPU, and time.

Dynamic pages load some or all of their content after the initial page load using JavaScript. The initial HTML response may contain only a skeleton layout with empty containers that JavaScript fills in by calling APIs in the background. Single-page applications built with React, Vue, or Angular are almost entirely dynamic. E-commerce product listings, social media feeds, search results with infinite scroll, and interactive dashboards are commonly dynamic as well.

For dynamic pages, you need a tool that can execute JavaScript. Playwright and Selenium both launch real browser instances that render the page fully, including all JavaScript execution. This lets you scrape content that only appears after user interactions like clicking buttons, scrolling, or waiting for AJAX calls to complete.

There is an important middle ground that many beginners overlook. Before reaching for a browser automation tool, inspect the target page's network requests using your browser's developer tools (the Network tab). Dynamic pages frequently fetch their data from backend API endpoints that return clean JSON. If you can identify these API endpoints, you can call them directly with Requests or httpx and skip browser automation entirely. This approach is faster, more reliable, and uses far fewer resources than running a headless browser. It works especially well for sites that use REST APIs or GraphQL endpoints to populate their pages.

As a general rule, start with the simplest tool that works. Try Requests first. If the data is not in the raw HTML, check for API calls in the Network tab. Only launch Playwright if you genuinely need JavaScript execution or browser interaction to reach the data.

Building Your First Python Scraper

A basic Python scraper using Requests and BeautifulSoup requires only a few lines of code. The process begins with installing the libraries using pip, Python's package manager. You install requests and beautifulsoup4, then import them into your script.

The first step is fetching the page. You call requests.get() with the target URL, which returns a Response object containing the HTML content. You should always check the response status code to confirm the request succeeded. A status code of 200 indicates success, while 403 means the server blocked the request, and 404 means the page was not found.

The second step is parsing the HTML. You pass the response text to BeautifulSoup's constructor along with a parser specification (typically "html.parser" for the built-in Python parser, or "lxml" for faster performance). This creates a soup object that represents the HTML document as a navigable tree.

The third step is locating and extracting data. You use methods like find(), find_all(), and select() to locate HTML elements by their tag, class, ID, or CSS selector. For example, soup.find_all("h2") returns every h2 element on the page, and soup.select(".product-price") returns every element with the CSS class "product-price". You then extract the text content or attribute values from these elements.

The fourth step is storing the extracted data. For quick scripts, you might print results to the console or write them to a CSV file using Python's built-in csv module. For more structured projects, you would store results in a pandas DataFrame or insert them into a database.

Error handling is important even in basic scrapers. Wrap your request calls in try/except blocks to handle connection errors, timeouts, and unexpected response formats. Set a reasonable timeout on your requests (10-30 seconds is typical) to prevent your scraper from hanging indefinitely on unresponsive servers. For a complete walkthrough with working code examples, see our Python Web Scraping Tutorial.

Handling Common Scraping Challenges

Real-world scraping rarely goes as smoothly as a tutorial example. Websites actively resist automated access, data is spread across multiple pages, and content structures change without warning. Understanding these challenges before you encounter them saves significant debugging time.

Anti-Bot Protections

Many websites implement measures to detect and block automated access. The most common protections include rate limiting (returning 429 status codes when you send too many requests), user-agent string validation (blocking requests that do not include a browser-like user-agent header), IP-based blocking (banning IP addresses that send high volumes of requests), JavaScript challenges (requiring JavaScript execution to prove the visitor is a real browser), and CAPTCHAs (visual or behavioral puzzles that are difficult for automated systems to solve).

Strategies for working around these protections include rotating user-agent strings from a pool of realistic browser identifiers, adding random delays between requests to mimic human browsing patterns, using proxy services to distribute requests across many IP addresses, and running headless browsers with stealth plugins that mask automation indicators. For details on handling JavaScript-rendered content, see Scraping Dynamic Pages with Python.

Pagination

Most websites split large datasets across multiple pages. Common pagination patterns include numbered page links (page=1, page=2), next-page buttons, infinite scroll triggered by scrolling to the bottom of the page, and API-based pagination using offset and limit parameters. Each pattern requires a different scraping approach. Numbered pages and next-page links are handled by extracting the next URL and looping through pages sequentially. Infinite scroll requires browser automation to trigger the scroll event and wait for new content to load. API pagination is often the simplest to handle, as you can increment an offset parameter in a loop. Our guide on scraping paginated sites in Python covers each of these patterns with examples.

Rate Limiting and Respectful Scraping

Sending requests too quickly can overwhelm a server, trigger anti-bot protections, or get your IP address banned permanently. Responsible scraping requires intentional pacing. Add a delay of at least one second between requests (time.sleep() in Python), or use Scrapy's AutoThrottle middleware, which dynamically adjusts request intervals based on server response times. Always check the target site's robots.txt file for crawl delay directives and respect them. If a server returns a 429 (Too Many Requests) response, back off exponentially before retrying.

Changing Website Structures

Websites redesign their HTML regularly, and when they do, scrapers that rely on specific CSS classes, IDs, or element hierarchies break. Minimizing maintenance requires writing selectors that are as stable as possible. Prefer data attributes and semantic HTML elements over fragile class names. Test your scrapers regularly with automated checks that verify the expected data structure. For long-running projects, consider Scrapling's adaptive parsing feature, which adjusts automatically to structural changes.

Storing and Exporting Scraped Data

Extracting data is only half the job. You also need to store it in a format that is useful for your downstream workflow. Python supports every common data storage format, and the right choice depends on the scale and structure of your data.

CSV files are the simplest option and work well for flat, tabular data. Python's built-in csv module handles reading and writing, and pandas can export DataFrames to CSV with a single method call. CSV files are easy to open in spreadsheet applications, share with non-technical colleagues, and import into databases. However, they do not handle nested data structures well, and large CSV files can be slow to read and write. For a step-by-step guide, see How to Export Scraped Data to CSV.

JSON files are better for hierarchical or nested data. If your scraped data has a natural tree structure (for example, a product listing with multiple variants, each with their own prices and attributes), JSON preserves that structure. Python's json module makes reading and writing JSON straightforward, and JSON is the native format for web APIs and many NoSQL databases.

SQLite is an excellent choice for medium-scale projects. It is a file-based relational database that requires no server setup, and Python includes SQLite support in its standard library. SQLite handles concurrent reads well, supports SQL queries for data analysis, and scales to datasets with millions of rows. For larger production workloads, PostgreSQL or MySQL provide better write concurrency and support for multiple simultaneous users.

pandas DataFrames are the most common intermediate format for data analysis workflows. You can accumulate scraped data in a DataFrame, clean it using pandas' extensive transformation functions, and then export it to CSV, Excel, JSON, SQL databases, or Parquet files. If your goal is to analyze scraped data rather than just store it, pandas is the natural choice.

Scrapy includes built-in feed exporters that automatically save crawled data to CSV, JSON, JSON Lines, or XML files without any custom code. For large-scale production systems, you might feed scraped data into message queues like RabbitMQ or Kafka for downstream processing, or write it directly to cloud data warehouses like BigQuery or Snowflake.

Web scraping exists in a complex legal space that varies by jurisdiction and by the specific data being collected. Before starting any scraping project, you should understand the legal landscape and adopt ethical practices that minimize risk.

In the United States, the primary law relevant to web scraping is the Computer Fraud and Abuse Act (CFAA). The landmark hiQ Labs v. LinkedIn case, which reached the Supreme Court, established that scraping publicly available data is not necessarily a CFAA violation. However, subsequent rulings have added nuance, and scraping data behind a login, ignoring explicit cease-and-desist notices, or violating a website's terms of service can still create legal liability.

In the European Union, the General Data Protection Regulation (GDPR) imposes strict requirements on the collection of personal data, regardless of whether that data is publicly visible. Scraping names, email addresses, phone numbers, or other personal identifiers from EU-based websites without a lawful basis can result in significant fines.

Best practices for legal and ethical scraping include checking the target site's robots.txt file and respecting its directives, reviewing the site's terms of service for explicit restrictions on automated access, using official APIs when they are available, avoiding the collection of personal data unless you have a clear legal basis, implementing rate limiting to avoid placing excessive load on servers, and identifying your scraper with a descriptive user-agent string rather than impersonating a browser.

When in doubt, consult a legal professional familiar with internet law in your jurisdiction. The legal landscape around web scraping continues to evolve, and what is permissible today may change with new legislation or court decisions.

Production Scraping Best Practices

Moving a scraper from a development script to a reliable production system requires attention to error handling, monitoring, scheduling, and maintenance.

Robust error handling is the foundation of a production scraper. Network errors, timeouts, unexpected HTML structures, and server-side changes can all cause failures. Use retry logic with exponential backoff for transient errors, log every failure with enough context to diagnose the problem later, and set up alerts for critical failures so you can respond quickly.

Data validation ensures that the data you collect is accurate and complete. Use libraries like pydantic to define expected data schemas and validate each scraped record against them. If a record fails validation, log it as a data quality issue rather than silently inserting bad data into your pipeline.

Scheduling determines when and how often your scraper runs. For simple cases, cron jobs on a Linux server work well. For more complex workflows with dependencies, retry logic, and monitoring dashboards, tools like Apache Airflow or Prefect provide a full orchestration platform. Cloud-based options like AWS Lambda or Google Cloud Functions can run scrapers on a schedule without managing server infrastructure.

Proxy rotation becomes essential for production scrapers that send high volumes of requests. Services like residential proxy networks distribute your requests across thousands of IP addresses, reducing the risk of any single address being blocked. Rotate proxies on every request or every few requests, and remove proxies that return errors from your pool automatically.

Containerization with Docker makes scrapers portable and reproducible. A Docker container bundles your Python code, its dependencies, and any browser binaries (for Playwright or Selenium) into a single deployable unit that runs identically on development machines, CI servers, and production infrastructure.

Version control and testing round out the production toolkit. Keep your scrapers in a Git repository, write tests that verify parsing logic against saved HTML fixtures, and run those tests in CI to catch regressions before they reach production. When a target site changes its structure, update your fixtures and selectors together in a single commit.

Explore Web Scraping with Python

Tutorials

Libraries and Tools

Techniques

Projects