Project Goals
3 min- Scrape today's price of a single product.
- Append a row to
price_log.csvonly when the price changes. - Print a friendly summary: current, all-time low, all-time high, week trend.
- Be polite — User-Agent, timeout, single request per run.
Warm-Up · Pick a Stable Target
5 minUse https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html — a friendly demo page that's scrape-safe. (Real shops change their HTML often and have ToS to read.)
Open the page in your browser, Inspect the price element, copy its CSS selector. For this page the price lives at p.price_color.
A "tracker" isn't one big scraper — it's a small scraper that runs often. The complexity is in storage + diffing, not in the network call.
Plan · Scrape, Diff, Log, Report
14 minStep 1 — Scrape current price
import requests from bs4 import BeautifulSoup URL = "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html" def fetch_price(): r = requests.get(URL, timeout=10, headers={"User-Agent": "advaslearning-py-l4 (price tracker demo)"}) r.raise_for_status() soup = BeautifulSoup(r.text, "lxml") raw = soup.select_one("p.price_color").get_text(strip=True) return float(raw.replace("£", ""))
Step 2 — Read price history
import csv from pathlib import Path LOG = Path("price_log.csv") FIELDS = ["date", "price"] def load_history(): if not LOG.exists(): return [] with LOG.open(newline="") as f: return [{"date": r["date"], "price": float(r["price"])} for r in csv.DictReader(f)]
Step 3 — Diff & append
from datetime import date def maybe_log(history, current): if history and history[-1]["price"] == current: return False if not LOG.exists(): with LOG.open("w", newline="") as f: csv.DictWriter(f, fieldnames=FIELDS).writeheader() with LOG.open("a", newline="") as f: csv.DictWriter(f, fieldnames=FIELDS).writerow( {"date": str(date.today()), "price": current} ) return True
Step 4 — Report
def report(history, current): prices = [h["price"] for h in history] or [current] lo, hi = min(prices), max(prices) print(f"💷 current : £{current:.2f}") print(f"📉 all-time low : £{lo:.2f}") print(f"📈 all-time high: £{hi:.2f}") if current == lo and len(prices) > 1: print("🎯 NEW LOW") elif current == hi: print("⬆️ new high")
Build · price_tracker.py
12 min# price_tracker.py — watch one product import csv, requests from bs4 import BeautifulSoup from datetime import date from pathlib import Path URL = "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html" LOG = Path("price_log.csv") FIELDS = ["date", "price"] HEADERS = {"User-Agent": "advaslearning-py-l4 (price tracker demo)"} def fetch_price(): r = requests.get(URL, timeout=10, headers=HEADERS) r.raise_for_status() soup = BeautifulSoup(r.text, "lxml") return float(soup.select_one("p.price_color").get_text(strip=True).replace("£", "")) def load_history(): if not LOG.exists(): return [] with LOG.open(newline="") as f: return [{"date": r["date"], "price": float(r["price"])} for r in csv.DictReader(f)] def maybe_log(history, current): if history and history[-1]["price"] == current: return False new_file = not LOG.exists() with LOG.open("a", newline="") as f: w = csv.DictWriter(f, fieldnames=FIELDS) if new_file: w.writeheader() w.writerow({"date": str(date.today()), "price": current}) return True def report(history, current): prices = [h["price"] for h in history] + [current] lo, hi = min(prices), max(prices) print(f"💷 current : £{current:.2f}") print(f"📉 low : £{lo:.2f}") print(f"📈 high : £{hi:.2f}") if len(prices) >= 2 and current < prices[-2]: print(f"🔻 dropped from £{prices[-2]:.2f}") elif current == lo and len(prices) > 1: print("🎯 NEW LOW") def main(): history = load_history() current = fetch_price() changed = maybe_log(history, current) if changed: print(f"📝 logged new price on {date.today()}") else: print("· unchanged from last run") report(history, current) if __name__ == "__main__": main()
Sample run sequence (3 days)
day 1 📝 logged new price on 2026-05-26 💷 current : £51.77 📉 low : £51.77 📈 high : £51.77 day 2 (price unchanged on the demo site) · unchanged from last run 💷 current : £51.77 ... day 3 (imagine a price drop) 📝 logged new price on 2026-05-28 🔻 dropped from £51.77 💷 current : £48.50 📉 low : £48.50 📈 high : £51.77 🎯 NEW LOW
Extensions
13 minRead a list of URLs from products.txt. Track each one to its own CSV named after the URL slug.
In report, also print the average and range of the last 7 logged prices.
On Windows: open Task Scheduler, create a daily task running python C:\\path\\to\\price_tracker.py. On macOS/Linux: add a cron line like 0 9 * * * /usr/bin/python3 ~/price_tracker.py.
Stretch · ntfy.sh Alert on Drop
8 minIf the price drops by ≥ 5%, POST a message to https://ntfy.sh/<your-secret-topic>. Now your phone gets a push when the price falls.
Show one possible solution
# add to price_tracker.py def alert(old, new): drop = (old - new) / old * 100 if drop < 5: return msg = f"📉 price drop {drop:.0f}% £{old:.2f} → £{new:.2f}" requests.post("https://ntfy.sh/advaslearning-prices-abc123", data=msg.encode("utf-8"), timeout=5) # inside main(), after maybe_log: if changed and history: alert(history[-1]["price"], current)
Non-negotiables: only alert on significant drops, secret topic name, plain-text body.
Recap
3 minYou combined three skills into one tiny app: scrape (Lesson 15), log (CSV from Lesson 4), schedule (your OS). The append-on-change pattern is core to monitoring of any kind — server health, stock prices, weather alerts. The shape is identical, only the scraper changes.
Homework
4 minPick three different demo products on books.toscrape.com. Track them all in one run, each to its own CSV under a logs/ folder. Print a one-line summary per product. Bonus: write a separate summary.py that reads all logs and prints which product has dropped most since you started tracking.
Sample skeleton
# track_many.py URLS = [ ("light-attic", "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"), ("tipping", "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"), ("soumission", "https://books.toscrape.com/catalogue/soumission_998/index.html"), ] from pathlib import Path Path("logs").mkdir(exist_ok=True) for slug, url in URLS: LOG = Path("logs") / f"{slug}.csv" # ... same fetch / load / maybe_log as the lesson print(f" {slug:<12} £{current:.2f}")
Non-negotiables: per-product CSVs in a folder, one row per product per change, polite sleeps between fetches.