Web Scraper: Building a Local Library of Linux Cheatsheets

Data Science & ML Projects (Beginner)

Chapter 1 · Web Scraper: Building a Local Library of Linux Cheatsheets

Real data science work starts long before any model gets trained — most of it is collecting and organizing data that doesn't yet exist in a clean, usable form. This chapter is deliberately the one project in this course with no modeling at all: a reusable tool that collects every document in one category from a listing site and builds a local library out of them.

What We're Building

A scraper that: fetches a category listing page, follows its pagination across every page in that category (not just the first, unlike py4-7's own single-page version), visits each item's own detail page, downloads the linked content, and saves everything into a local folder plus a JSON manifest describing what was collected.

It's built and tested here against books.toscrape.com — the sister sandbox to py4-7's own quotes.toscrape.com, from the same project, explicitly built and maintained for scraping practice. The tool itself is generic by design: point the finished script at a real, permitted Linux-cheatsheet site (after checking that site's own robots.txt and Terms of Service, per this chapter's own warn-box) and it collects cheatsheets exactly the same way it collects practice-site book listings here.

Step 1: Fetching One Category's Listing Page

import requests
from bs4 import BeautifulSoup

CATEGORY_URL = "https://books.toscrape.com/catalogue/category/books/travel_2/index.html"

response = requests.get(CATEGORY_URL)
soup = BeautifulSoup(response.text, "html.parser")
items = soup.find_all("article", class_="product_pod")
print(len(items))   # one 
per item on this page

Same requests.get() + BeautifulSoup(...).find_all() pattern py4-7 already introduced — one page fetched, one set of matching elements found.

Step 2: Following Pagination Across the Whole Category

import time

def get_all_items(start_url):
    items = []
    url = start_url
    while url:
        response = requests.get(url)
        soup = BeautifulSoup(response.text, "html.parser")
        items.extend(soup.find_all("article", class_="product_pod"))

        next_link = soup.find("li", class_="next")
        url = requests.compat.urljoin(url, next_link.find("a")["href"]) if next_link else None
        time.sleep(1)   # one polite pause per page, per this chapter's own warn-box

    return items

A while loop keeps following the page's own "Next →" link (soup.find("li", class_="next")) until there isn't one — None ends the loop. requests.compat.urljoin() turns the relative link on the page into a full URL. time.sleep(1) between requests is the same discipline py4-7's own warn-box named, applied here for real since this chapter actually fetches multiple pages.

Step 3: Visiting Each Item's Own Detail Page

def get_detail(item, base_url):
    link = item.find("h3").find("a")["href"]
    detail_url = requests.compat.urljoin(base_url, link)

    response = requests.get(detail_url)
    time.sleep(1)
    soup = BeautifulSoup(response.text, "html.parser")

    return {
        "title": soup.find("h1").get_text(),
        "price": soup.find("p", class_="price_color").get_text(),
        "url": detail_url,
    }

Each item on the listing page only has a title and a link — the fuller record lives on its own detail page. get_detail() follows that link and pulls out the specific fields worth keeping, exactly the "visit the linked page for the real content" step a real cheatsheet-collector would need.

Step 4: Saving the Manifest

import json
from pathlib import Path

def save_manifest(records, path="library_manifest.json"):
    Path(path).write_text(json.dumps(records, indent=2))
    print(f"Saved {len(records)} records to {path}.")

The same pathlib + json save pattern py4-7 used — a manifest is just data, and it persists the same way any other collected data does.

The Complete Scraper

import json, time, requests
from bs4 import BeautifulSoup
from pathlib import Path

START_URL = "https://books.toscrape.com/catalogue/category/books/travel_2/index.html"

def get_all_items(start_url):
    items, url = [], start_url
    while url:
        soup = BeautifulSoup(requests.get(url).text, "html.parser")
        items.extend(soup.find_all("article", class_="product_pod"))
        next_link = soup.find("li", class_="next")
        url = requests.compat.urljoin(url, next_link.find("a")["href"]) if next_link else None
        time.sleep(1)
    return items

def get_detail(item, base_url):
    link = item.find("h3").find("a")["href"]
    detail_url = requests.compat.urljoin(base_url, link)
    soup = BeautifulSoup(requests.get(detail_url).text, "html.parser")
    time.sleep(1)
    return {
        "title": soup.find("h1").get_text(),
        "price": soup.find("p", class_="price_color").get_text(),
        "url": detail_url,
    }

items = get_all_items(START_URL)
records = [get_detail(item, START_URL) for item in items]
Path("library_manifest.json").write_text(json.dumps(records, indent=2))
print(f"Saved {len(records)} records.")
Scraping ethics, revisited from py4-7
Every point py4-7's own warn-box made still applies, and matters more here since this chapter fetches many pages, not one: check the target site's own /robots.txt and Terms of Service before scraping it, keep a real delay between requests (time.sleep(), used twice above — once per listing page, once per detail page), and never point this tool at a site that hasn't given permission. books.toscrape.com is used here specifically because it's built for this practice — that permission does not transfer automatically to a real Linux-cheatsheet site. Find one that explicitly allows scraping, or one offering its own downloadable archive/API, before reusing this exact script for the real goal.

Pagination loops

A while loop following a "Next" link until there isn't one.

requests.compat.urljoin()

Turns a relative link on a page into a full, fetchable URL.

Two-level scraping

Listing pages for links, detail pages for the real content.

Manifest files

A JSON record of exactly what a scraper collected and from where.

Extend This Project

Try these on your own:

  • Point this exact script at a real, permitted Linux-cheatsheet source you've checked robots.txt/ToS for — this is the actual real-world goal the practice run above was rehearsing.
  • Wrap each requests.get() call in try/except to skip a failed page instead of crashing the whole run.
  • Save each item's own detail-page HTML (or linked file) to disk as a separate file, named from its own title, alongside the manifest — the actual "local library," not just metadata about one.
  • Add a command-line argument (argparse) for the category URL, so the same script works against any category without editing the code.

What's Next

Chapter 2: Data Cleaning Pipeline: Wrangling a Messy Real-World CSV — turning ds1-4's own cleaning toolkit into a real, reusable pipeline, run against a genuinely messy dataset instead of a small illustrative one.

Chapter 1 Quick Reference

  • Deliberately model-free — data collection alone is real, substantial data science work, not a preliminary step to rush through
  • Extends py4-7's own single-page scraper into a genuine multi-page, two-level (listing + detail) collection tool
  • Pagination: follow "Next" links in a loop until none remain, with a polite delay per request
  • Built and tested on books.toscrape.com's own safe practice sandbox; the real target (a permitted cheatsheet site) is the reader's own next step
  • Next chapter: Data Cleaning Pipeline: Wrangling a Messy Real-World CSV