~ / guides / Python Web Scraping: The Complete Guide

Python Web Scraping: The Complete Guide

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Python is the most practical language for web scraping: the library stack is the deepest and the examples are everywhere.
  • For static HTML, requests + BeautifulSoup scrapes a page in about 15 lines. Scrapy handles large crawls, Playwright handles JavaScript.
  • I scraped 1,000 books across 50 pages on books.toscrape.com with the code below to confirm every snippet runs.
  • Parsing is the easy part. Staying unblocked at scale is the real work, and that is what a scraper API solves.

I have written Python scrapers for years to feed price trackers and market-research tools, and Python is still the first thing I open for a new job. This guide is the map I wish I had on day one: whether Python is the right pick, which library does what, a full requests + BeautifulSoup walkthrough you can run, how to handle JavaScript-heavy pages, and how to keep a scraper alive once a site starts pushing back. Every code sample here ran against books.toscrape.com, a sandbox built for practice, in June 2026.

Is Python good for web scraping?

Yes, Python is the most practical language for web scraping, and it is the one I recommend to almost everyone. The reason is the ecosystem: requests, BeautifulSoup, Scrapy, lxml, and Playwright cover every stage of a scrape, from fetching a page to rendering JavaScript, and they are all mature and well documented. When something breaks at 2am, the answer is usually already on Stack Overflow.

Three things make Python win here:

Python is slower than Go or Rust on raw CPU work, but scraping is almost always bound by network and by how fast a site lets you request, not by your language. That speed gap rarely matters in practice.

What are the main Python web scraping libraries?

The four tools below cover the whole field, and the right one depends on the site. Static HTML needs only a fetcher and a parser. Large crawls need a framework. JavaScript-rendered pages need a real browser.

LibraryWhat it doesBest forRuns JavaScript
requests + BeautifulSoupFetches pages, parses HTMLStatic sites, small to medium jobs, learningNo
ScrapyFull crawling framework with concurrency and pipelinesLarge crawls, thousands of pages, structured projectsNo
Selenium / PlaywrightAutomates a real browserJavaScript-heavy sites, logins, clickingYes
lxmlFast, low-level HTML/XML parserSpeed on huge documents, XPath queriesNo

A few notes from using all four:

For everything that follows I use requests + BeautifulSoup, because it is the foundation the rest build on.

How do you scrape a website with Python step by step?

The loop is three steps: install the libraries, fetch the page with requests, then parse the HTML with BeautifulSoup and pull the fields you want. Here is each step against a live page.

Step 1: Install the libraries

Install both with pip in one command:

pip install requests beautifulsoup4

beautifulsoup4 is the package name. The import in your code is bs4. You do not need a separate parser because html.parser ships with Python.

Step 2: Fetch and parse one page

This script fetches the first catalogue page, then pulls the title and price for every book on it:

import requests
from bs4 import BeautifulSoup

url = "https://books.toscrape.com/catalogue/page-1.html"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"})
resp.raise_for_status()

soup = BeautifulSoup(resp.text, "html.parser")

books = []
for card in soup.select("article.product_pod"):
    books.append({
        "title": card.h3.a["title"],
        "price": card.select_one(".price_color").get_text(strip=True),
    })

print(f"{len(books)} books on this page")
for b in books[:5]:
    print(f"{b['price']:>8}  {b['title']}")

When I ran it, it found all 20 books on the page and printed the first five:

20 books on this page
 £51.77  A Light in the Attic
 £53.74  Tipping the Velvet
 £50.10  Soumission
 £47.82  Sharp Objects
 £54.23  Sapiens: A Brief History of Humankind

Three things from that code are worth keeping:

Step 3: Set a User-Agent

Notice the headers={"User-Agent": "Mozilla/5.0"} in the request. By default requests announces itself as python-requests, which many sites block on sight. Sending a normal browser User-Agent is the single cheapest step to look less like a bot, and you should do it on every real scrape.

How do you handle pagination in Python?

Follow the “next” link until it disappears. Most paginated sites put a next-page anchor at the bottom of each list, so you scrape a page, look for that link, build the next URL, and repeat until there is no next link.

This script walks the entire books.toscrape.com catalogue and counts both pages and books:

import requests
from bs4 import BeautifulSoup

BASE = "https://books.toscrape.com/catalogue/"
url = BASE + "page-1.html"
pages = 0
total = 0

while url:
    soup = BeautifulSoup(requests.get(url).text, "html.parser")
    total += len(soup.select("article.product_pod"))
    pages += 1
    nxt = soup.select_one("li.next a")
    url = BASE + nxt["href"] if nxt else None

print(f"pages crawled: {pages}")
print(f"total books:   {total}")

It crawled the full catalogue and returned every book:

pages crawled: 50
total books:   1000

On a real target, add time.sleep(1) inside the loop so you do not hammer the server, and wrap the request in a try/except so one failed page does not kill the whole run. The pattern itself scales to any “next button” pagination.

How do you scrape JavaScript-heavy sites in Python?

Use a browser automation tool like Playwright, because requests cannot run JavaScript. When a page loads its data with JavaScript after the initial response, that data is not in the HTML requests downloads, so BeautifulSoup sees an empty shell. The fix is to render the page in a real browser first, then read the rendered HTML.

Here is the shape of a Playwright scrape. Install it with pip install playwright followed by playwright install chromium:

from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    page.wait_for_selector(".product")   # wait for JS to inject content
    html = page.content()
    browser.close()

soup = BeautifulSoup(html, "html.parser")

The pattern stays the same as before: get HTML, hand it to BeautifulSoup, select your fields. Playwright just produces the HTML after JavaScript has run. The cost is speed and resources, since you are driving a full browser, so use it only when a site genuinely needs it. The quick test is to fetch a page with requests and search the response for a value you can see in the browser. When it is missing, the page is JavaScript-rendered and you reach for Playwright.

How do you avoid getting blocked when scraping in Python?

Look like a normal browser, slow down, and spread requests across IP addresses. A site flags scrapers by three signals: requests that arrive too fast, headers that look automated, and too much traffic from one IP. Each has a countermeasure.

Block signalWhat triggers itFix
Bad headersDefault python-requests User-Agent, no headersSend a realistic User-Agent and browser-like headers
Request rateDozens of requests per second from one clientAdd time.sleep(), respect 429 responses, back off
Single IPThousands of requests from one addressRotate IPs or proxies, or use a scraper API
No JavaScriptHeadless requests that never run scriptsRender with Playwright when the site checks

These measures carry a small project a long way. The trouble starts at scale: rotating a large IP pool, solving CAPTCHAs, and handling per-site quirks becomes its own engineering job. That is where a scraper API like ChocoData fits the category, with a universal endpoint plus dedicated endpoints that return the page so your Python code keeps working unchanged while the blocking is handled for you. I cover that full tradeoff, and the legal side, in the web scraping guide.

For the scraping itself, the stack in this guide will carry you a long way. Take the pagination script, point it at a site you care about, adjust the selectors until the fields come out clean, and add the anti-block measures as the target pushes back.

FAQ

Is Python or JavaScript better for web scraping?

Python for most jobs. The library ecosystem (requests, BeautifulSoup, Scrapy, Playwright) is the deepest and the tutorials are everywhere. JavaScript with Node and Puppeteer is a strong second, mainly when your team already lives in Node or you need a browser by default.

Do I need to know Python to scrape a website?

Not for tiny jobs. No-code browser extensions let you click the fields you want. For anything that runs on a schedule, handles pagination, or fights blocks, Python gives you far more control, and you can learn enough to scrape a static site in an afternoon.

How many requests per second is safe when scraping in Python?

There is no universal number. Start at one request per second with a sleep between calls, read the site's robots.txt and terms, and back off the moment you see CAPTCHAs or 429 responses. At higher volumes, rotate IPs or use a scraper API that paces requests for you.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.