Scraping real pages with requests

Beautiful Soup · Practical · 12 min

Beautiful Soup is almost always paired with an HTTP client, and the glue code decides whether the scraper is correct and polite.

A minimal scraper: fetch with requests, check the status, parse, extract.

  • Use a requests.Session() to reuse connections and set headers once, including an honest User-Agent with contact info.
  • Always pass a timeout= and call resp.raise_for_status() so errors don't parse as content.
  • Pass resp.content (bytes) to Beautiful Soup and let it detect the encoding from the document; resp.text relies on HTTP headers that are often wrong.

Links in pages are often relative (/p/2, ../img.png). Resolve them against the page URL with urllib.parse.urljoin(resp.url, href); use resp.url rather than the URL you requested, since redirects may have moved you.

Pagination usually means following a "next" link until there isn't one.

Be a good citizen:

  • Check robots.txt (the standard library's urllib.robotparser reads it) and the site's terms.
  • Rate-limit with a delay between requests and avoid parallel hammering.
  • Prefer an official API or a JSON endpoint the page already calls (look in the browser's network tab), which is more stable than HTML.
  • Cache pages while developing so you don't refetch on every run.

If the content isn't in the HTML at all because JavaScript renders it, Beautiful Soup can't see it; see the lesson on robust scrapers.

Example

import time
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup

session = requests.Session()
session.headers["User-Agent"] = "price-bot/1.0 (+mailto:me@example.com)"

def scrape_titles(start_url, max_pages=3):
    url, titles = start_url, []
    for _ in range(max_pages):
        resp = session.get(url, timeout=10)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.content, "html.parser")
        titles += [a.get_text(strip=True) for a in soup.select("h3 a")]
        nxt = soup.select_one("li.next a")
        if nxt is None:
            break
        url = urljoin(resp.url, nxt["href"])     # resolve relative link
        time.sleep(1)                             # be polite
    return titles

print(scrape_titles("https://books.toscrape.com/")[:2])
# ['A Light in the ...', 'Tipping the Velvet']

Remember

  • requests.Session with User-Agent, timeout and raise_for_status
  • Pass resp.content (bytes) so Beautiful Soup detects the encoding
  • urljoin(resp.url, href) for relative links
  • Respect robots.txt, rate-limit, and prefer APIs when they exist

Watch out for

  • Building next-page URLs by string concatenation and breaking on relative paths.

Check yourself

  1. Why use urljoin(resp.url, href) instead of start_url + href?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.crummy.com/software/BeautifulSoup/bs4/doc/#quick-start