Tech With Galvan
Back to Articles
Development11/10/2025 • 3 min read

Web Scraper using Python and BeautifulSoup

Extract data from websites with Python and BeautifulSoup — headlines, prices, and links scraped politely, with pagination and CSV export.

Galvan
Galvan

Founder & Creator

Introduction

Web scraping is the skill that turns the internet into your database. This tutorial builds a polite, working scraper with BeautifulSoup: fetch a page, parse the HTML, extract structured data (headlines, links, prices), follow pagination, and export everything to CSV. The same request-and-parse loop powers the weather app — except there the API returned JSON, and here you parse HTML yourself.

The polite part matters: respecting robots.txt, rate limiting, and identifying your scraper are what separate a tool from a nuisance.

Features

  • CSS selector extraction — target any element precisely.
  • Pagination support — follow ?page=N links automatically.
  • Polite crawling — 1-second delays, custom User-Agent, robots.txt awareness.
  • Structured output — results as a list of dicts, saved to CSV.
  • Error resilience — one bad page doesn’t kill the run.

Prerequisites

pip install requests beautifulsoup4 pandas

Step 1: Create the Script

Save as scraper.py — this example scrapes quote authors and texts from quotes.toscrape.com (a site built for scraping practice):

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time

HEADERS = {"User-Agent": "MyLearningScraper/1.0 (contact: [email protected])"}


def scrape_page(url):
    response = requests.get(url, headers=HEADERS, timeout=10)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    results = []
    for quote in soup.select("div.quote"):
        results.append({
            "text": quote.select_one("span.text").get_text(strip=True),
            "author": quote.select_one("small.author").get_text(strip=True),
            "tags": ",".join(t.get_text() for t in quote.select("a.tag")),
        })

    next_link = soup.select_one("li.next a")
    next_url = "https://quotes.toscrape.com" + next_link["href"] if next_link else None
    return results, next_url


def scrape_all(start_url, max_pages=5):
    all_rows, url, page = [], start_url, 1
    while url and page <= max_pages:
        print(f"Scraping page {page}: {url}")
        try:
            rows, url = scrape_page(url)
            all_rows.extend(rows)
        except requests.RequestException as e:
            print(f"  Skipping ({e})")
            break
        page += 1
        time.sleep(1)  # be polite
    return all_rows


if __name__ == "__main__":
    data = scrape_all("https://quotes.toscrape.com/")
    df = pd.DataFrame(data)
    df.to_csv("quotes.csv", index=False)
    print(f"Saved {len(df)} rows to quotes.csv")
    print(df.head())

Step 2: Run the Scraper

python scraper.py

Watch it page through the site, then open quotes.csv — structured data extracted from raw HTML.

How It Works

BeautifulSoup turns raw HTML into a navigable tree, and soup.select() queries it with CSS selectors — the same syntax you use in stylesheets. div.quote finds containers; span.text finds the headline inside each container. The two-level pattern (select containers, then extract fields per container) is the fundamental scraping loop, and it maps directly to how you’d read the page in browser DevTools: right-click an element, Copy selector, adapt.

Pagination is recursion-lite: each page’s HTML contains a li.next a link to the following page. Following it until None walks the whole site — with max_pages as a safety brake, because real sites have surprising link structures.

The politeness layer has three parts: a custom User-Agent that identifies the scraper (many sites block default python-requests), time.sleep(1) between requests so you don’t hammer the server, and try/except so a timeout on page 14 doesn’t discard pages 1–13.

Common Errors & Fixes

  • AttributeError: 'NoneType' object has no attribute 'get_text' — your selector matched nothing on some page; guard each extraction or use select_one with a check, as the code does per-container.
  • 403 Forbidden — the site blocks your User-Agent; send a descriptive one (never scrape LinkedIn or similar — they actively sue).
  • Empty results on JS-heavy sites — BeautifulSoup parses the initial HTML; if content loads via JavaScript, you need Selenium or Playwright to render first.
  • Encoding garbage — pass response.content (bytes) to BeautifulSoup and let it detect encoding, rather than response.text.

Key Concepts

  • CSS selectors — precise element targeting without regex.
  • Container loop — select containers, extract fields within.
  • Pagination as a linked list — follow next-links until they vanish.
  • Rate limiting — one second per page keeps you welcome.

What to Try Next

  • Scrape a product page — price, rating, and availability with the same loop.
  • Add robots.txt checking with urllib.robotparser before each domain.
  • Schedule the scraper with cron on your home server and chart price history with the dashboard.
  • Add a Streamlit front-end — paste a URL, see the extracted table, download CSV.

FAQ

Scraping public data is generally legal in many jurisdictions, but terms of service, copyright, and personal-data laws (GDPR) create real exceptions. Check robots.txt, avoid personal data, and prefer official APIs whenever one exists.

When should I use an API instead?

Always when one exists — APIs are stable, documented, and sanctioned. Scraping is for when the data exists only in HTML.

How do I scrape sites that need login?

Use requests.Session() to maintain cookies through the login POST, then scrape authenticated pages. Only do this where the terms permit it.