Jake Groszewski

Blog

Python Data Mining Tutorial: Extract, Clean, and Analyze Web Data with BeautifulSoup and Pandas

Python Data Mining Tutorial: Extract, Clean, and Analyze Web Data with BeautifulSoup and Pandas

Data mining is one of the most practical skills a Python developer can have. Whether you're tracking prices, aggregating news headlines, or building datasets for machine learning, the ability to pull structured data from the web and make sense of it is immediately useful.

This tutorial walks through a complete pipeline: scraping HTML content with BeautifulSoup, cleaning and structuring that data with Pandas, and running basic analysis to surface meaningful insights. By the end, you'll have a reusable pattern you can adapt to almost any data source.

Prerequisites

You'll need Python 3.8+ and the following packages:

pip install requests beautifulsoup4 pandas lxml

No prior experience with web scraping is required, but familiarity with Python functions and basic Pandas concepts will help.

Step 1: Understand the Target Page Structure

Before writing a single line of code, inspect the page you want to scrape. Open the page in your browser, right-click an element you care about, and choose Inspect. Look for consistent patterns in the HTML: class names, tag types, and nesting structures.

For this tutorial, we'll scrape a publicly available books dataset from books.toscrape.com, a site built specifically for practicing web scraping.

Step 2: Fetch the Page with Requests

Start by fetching the raw HTML:

import requests
from bs4 import BeautifulSoup

BASE_URL = "http://books.toscrape.com/catalogue/"
START_URL = "http://books.toscrape.com/catalogue/page-1.html"

def get_soup(url: str) -> BeautifulSoup:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return BeautifulSoup(response.text, "lxml")

Using raise_for_status() ensures you catch HTTP errors early rather than parsing an error page silently.

Step 3: Parse Book Data from the HTML

Each book on the page is contained in an <article> tag with the class product_pod. Extract the title, price, rating, and availability:

RATING_MAP = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}

def parse_books(soup: BeautifulSoup) -> list[dict]:
    books = []
    for article in soup.select("article.product_pod"):
        title = article.h3.a["title"]
        price_text = article.select_one(".price_color").text.strip()
        price = float(price_text.replace("Â", "").replace("£", ""))
        rating_word = article.p["class"][1]
        rating = RATING_MAP.get(rating_word, 0)
        availability = article.select_one(".availability").text.strip()
        books.append({
            "title": title,
            "price": price,
            "rating": rating,
            "availability": availability,
        })
    return books

Note the price cleaning step. Scraped text often contains encoding artifacts that need to be stripped before converting to a numeric type.

Step 4: Paginate Across Multiple Pages

The site has 50 pages of books. Loop through them by detecting the "next" button:

def scrape_all_books() -> list[dict]:
    all_books = []
    url = START_URL

    while url:
        soup = get_soup(url)
        all_books.extend(parse_books(soup))
        next_btn = soup.select_one("li.next a")
        if next_btn:
            url = BASE_URL + next_btn["href"]
        else:
            url = None

    return all_books

This pattern follow the "next" link until it disappears, works for the majority of paginated sites.

Step 5: Load Data into a Pandas DataFrame

With raw data collected, move it into Pandas for cleaning and analysis:

import pandas as pd

books = scrape_all_books()
df = pd.DataFrame(books)
print(df.shape)      # (1000, 4)
print(df.dtypes)
print(df.head())

Expected output:

(1000, 4)
title           object
price          float64
rating           int64
availability    object
dtype: object

Step 6: Clean and Validate the Data

Even well-structured scrapes produce dirty data. Check for nulls, duplicates, and outliers:

# Check for missing values
print(df.isnull().sum())

# Drop duplicates if any
df = df.drop_duplicates(subset=["title"])

# Confirm availability values
print(df["availability"].value_counts())

# Filter to in-stock books only
df = df[df["availability"] == "In stock"]

In most real-world scraping projects, data cleaning takes longer than the actual scraping. Build cleaning steps as discrete, testable functions whenever possible.

Step 7: Analyze the Data

Now the useful part... surfacing patterns in the dataset.

Price Distribution by Rating

price_by_rating = df.groupby("rating")["price"].agg(["mean", "median", "count"])
print(price_by_rating.round(2))

Sample output:

        mean  median  count
rating
1      35.47   34.44    196
2      37.82   36.99    198
3      37.24   35.00    209
4      37.80   36.57    203
5      36.50   34.61    194

Interesting: price and rating have almost no correlation on this dataset. A useful reminder that higher price doesn't mean higher quality.

Top 10 Most Affordable 5-Star Books

top_rated = df[df["rating"] == 5].sort_values("price").head(10)
print(top_rated[["title", "price"]])

Summary Statistics

print(df["price"].describe())

Step 8: Export Results

Save your cleaned dataset for reuse or further analysis:

df.to_csv("books_data.csv", index=False)

You can also export directly to Excel:

df.to_excel("books_data.xlsx", index=False)

Handling Common Scraping Issues

Rate limiting: Add a short delay between requests with time.sleep(1) to avoid hitting servers too aggressively.

User-Agent headers: Some sites block the default python-requests user agent. Pass a browser-like header:

headers = {"User-Agent": "Mozilla/5.0"}
response = requests.get(url, headers=headers, timeout=10)

Dynamic content: BeautifulSoup only parses static HTML. If data is loaded via JavaScript, you'll need Selenium or Playwright instead.

robots.txt: Always check a site's robots.txt file before scraping. Only scrape data you have a legitimate reason to access.

Extending the Pipeline

This pipeline is intentionally minimal. Here's how to extend it:

  • Visualize results with Plotly Dash or Matplotlib for interactive charts
  • Store data in a SQLite or PostgreSQL database instead of CSV
  • Schedule scrapes with a cron job or a tool like APScheduler
  • Add logging with Python's built-in logging module to track runs
  • Wrap it in a CLI using Click so you can pass arguments like page count or output format

Key Takeaways

  • Use requests + BeautifulSoup for static HTML pages; switch to Playwright or Selenium for JavaScript-rendered content.
  • Clean data as close to the source as possible — fix encoding issues before loading into Pandas.
  • The pagination loop pattern (follow next until gone) applies to most paginated sites.
  • Always check robots.txt and scrape responsibly with rate limiting.
  • Pandas groupby and agg are your best friends for quick exploratory analysis.

This pattern: fetch, parse, clean, analyze, export, forms the foundation of most data mining workflows. Once you have it down, you can apply it to almost any structured web source.