Kiet Nguyen logo
NotesNotesResumeResume
© 2026 Kiet Nguyen
← All notes
Pop art Swiss punk web of HTML wires snatching data — web scraping as leverage

How-to·May 12, 2026·11 min read

Why web scraping is high-leverage (and how to do it in Python)

Python · Web Scraping · Automation · Data · Career · Learning

Web scraping is a force multiplier for career research, personal finance signals, and technical curiosity—if you treat the web as a messy database, scrape with Python deliberately, and build a pipeline after the fetch. Philosophy, mental models, techniques, and what to do next.

“Why is web scraping the highest-leverage task in your career, your finance, and—yes—why is it exciting?”

Because most of the world’s semi-public knowledge still arrives as HTML, not as a polite API key in your inbox. If you can fetch, parse, store, and re-query that surface honestly, you stop waiting for dashboards other people designed for their product—and start building feeds of facts for your decisions.

This how-to is not a permission slip to hammer sites, steal content, or ignore the law. It is a map of leverage, mental models, Python techniques, what is worth scraping, and what to do after the scrape—so the excitement is engineering, not recklessness.

Respect robots.txt, site terms, copyright, privacy, and rate limits. Prefer official APIs and data exports when they exist. “I can scrape it” ≠ “I should scrape it.” When in doubt, slow down, authenticate only as allowed, and read the rules for that host.

What you will learn

  1. A philosophy of the web as a distributed, hostile-friendly database.
  2. Why scraping compounds in career, money, and curiosity.
  3. A Python path: from one httpx + parser script to browser automation and scale.
  4. What to scrape (and what to skip).
  5. What to do after—clean, store, validate, automate, monitor.
  6. Failure modes that kill leverage (brittleness, bans, legal mess, data hoarding).

One-sentence crystal

Web scraping is turning public (or permitted) web surfaces into structured, versioned, queryable data on a schedule you control—so decisions about jobs, prices, markets, docs, and competitors run on your tables, not on memory and browser tabs.


Part I — Philosophy: why this is leverage (and exciting)

Mental model 1 — The web is a database with a terrible query language

Every page is a row (or a nested document) in a global store. SQL would be nice. What you get is HTTP + HTML + JavaScript + cookies + whims.

Scraping is ETL for the open web:

  Extract   →  GET / fetch / render
  Transform →  parse DOM / clean / type
  Load      →  CSV / SQLite / Parquet / warehouse

If you already think in pipelines (CI logs, host inventories, unit files), scraping is the same craft on a messier substrate. That is why it feels exciting: you are debugging reality’s API.

Mental model 2 — APIs are privileges; HTML is the common denominator

  • Official APIs — stable, documented, often rate-limited and paid. Use them first.
  • HTML — what the browser sees; changes without a changelog; still the universal export format of institutions that never shipped a developer portal.

Leverage is not “always scrape.” Leverage is being able to when the API is missing, throttled, or incomplete—and knowing when to walk away.

Mental model 3 — Information asymmetry is a career and money tax

DomainWithout scraping / automationWith a disciplined scrape pipeline
CareerManually refresh job boards, miss postings, forget which JD changedStructured roles, companies, stacks, salary bands, “first seen” dates
FinanceScreenshots of prices, FOMO from socialYour own series: prices, fees, FX, local listings—auditable and plottable
Learning / opsDocs you half-rememberDiffs of changelogs, status pages, release notes on your watchlist

You are not guaranteed alpha. You are reducing search cost and forgetting cost—the silent taxes on every knowledge worker.

Mental model 4 — Scraping is a conversation with a stranger’s server

Good guests:

  • Identify themselves (User-Agent that is honest, contact if you run a serious bot).
  • Wait between requests.
  • Cache.
  • Stop when asked (robots.txt, 429, 403, legal notice).
  • Don’t punch login walls, CAPTCHAs, or paywalls as a sport.

Bad guests treat “public URL” as “infinite free warehouse.” That burns IP reputation, jobs, and sometimes courts. Leverage without ethics is just deferred incident response.

Mental model 5 — The product is the pipeline, not the one-off dump

A CSV you built once at 2 a.m. is a souvenir. A job that:

  1. fetches
  2. parses with tests
  3. writes with schema
  4. diffs against yesterday
  5. alerts on change

…is a system. Career and finance care about systems.

Why it’s exciting (without the hype voice)

  • Immediate feedback — wrong selector → empty list → fix → data appears. Tight loop.
  • Cross-domain — same skills serve research, shopping, OSINT-lite, monitoring, journalism of your own life.
  • Composes with everything — shell, Python, SQLite, pandas, cron, systemd timers, LLMs after structure (see structured extraction notes on this site).
  • Reveals how products think — pagination, anti-bot, SSR vs SPA—you learn web architecture by adversarial reading.

Part II — What to scrape (high signal) vs what to skip

Worth scraping (examples)

TargetCareerFinanceCraft
Job boards / career pages (where allowed)Role demand, stack keywords—Parsing cards, pagination
Public price lists / catalogs—Track SKUs, local marketsTables, variants
Release notes / changelogsTool versions on résumés & labs—Diff-friendly text
Status pagesDependency risk for your stack—Simple HTML, high value
Public regulatory or open-data portalsDomain literacyCompliance / macro contextOften bulk download > scrape
Your own sites & stagingPortfolio QA—Full control, perfect lab
Documentation indicesLearning paths—Link graphs

Prefer not to scrape (or only with clear rights)

  • Anything behind auth you don’t own or aren’t licensed to automate.
  • Personal data at scale (privacy law is not a vibe check).
  • Full mirror of copyrighted libraries “for later.”
  • Sites that offer a good API or bulk dump you ignored out of habit.
  • Real-time trading against ToS that forbid it—exciting until the ban hammer.

Filter question: Will this dataset change a decision I make weekly? If no, you are collecting stamps.


Part III — Python path: from one script to a small machine

Level 0 — Prefer the boring door

Before Beautiful Soup:

  1. Official API / CSV export / RSS / sitemap.
  2. curl / browser DevTools → Network — maybe the “page” is already JSON from an XHR. Scrape that endpoint (still obey rules).

If the site’s own frontend loads https://example.com/api/items?page=2, your scraper should too—HTML is plan B.

Level 1 — HTTP + parse (static HTML)

Stack that stays honest for years:

PieceRole
httpx (or requests)HTTP client; prefer httpx for timeouts/HTTP2 ergonomics
beautifulsoup4 + lxmlFriendly DOM queries
selectolaxFaster CSS-oriented parsing when volume grows
pydantic or dataclassesSchema for rows (fail loud)

Minimal pattern:

import httpx
from bs4 import BeautifulSoup

URL = "https://example.com/jobs"  # replace with a site you may fetch
HEADERS = {
    "User-Agent": "ResearchBot/0.1 (+https://yoursite.example/bot; contact@you.example)",
    "Accept-Language": "en",
}

def fetch(url: str) -> str:
    with httpx.Client(headers=HEADERS, timeout=30.0, follow_redirects=True) as client:
        r = client.get(url)
        r.raise_for_status()
        return r.text

def parse_jobs(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "lxml")
    rows = []
    for card in soup.select("article.job-card"):  # invent selectors per site
        title = card.select_one("h2")
        company = card.select_one(".company")
        link = card.select_one("a[href]")
        if not title or not link:
            continue
        rows.append({
            "title": title.get_text(strip=True),
            "company": company.get_text(strip=True) if company else None,
            "url": link["href"],
        })
    return rows

if __name__ == "__main__":
    data = parse_jobs(fetch(URL))
    print(len(data), "rows")
    for row in data[:5]:
        print(row)

Selectors are contracts. Prefer stable attributes (data-testid, semantic tags) over div > div > span:nth-child(3).

Level 2 — Pagination, sessions, politeness

import time
from urllib.parse import urljoin

def crawl_pages(start: str, max_pages: int = 5) -> list[dict]:
    out: list[dict] = []
    url = start
    with httpx.Client(headers=HEADERS, timeout=30.0, follow_redirects=True) as client:
        for page in range(max_pages):
            r = client.get(url)
            r.raise_for_status()
            batch = parse_jobs(r.text)
            if not batch:
                break
            out.extend(batch)
            # find "next" — site-specific
            soup = BeautifulSoup(r.text, "lxml")
            nxt = soup.select_one("a.next[href]")
            if not nxt:
                break
            url = urljoin(url, nxt["href"])
            time.sleep(1.5)  # be a guest
    return out

Also learn:

  • Conditional requests — ETag / If-Modified-Since when servers support them.
  • Disk cache — don’t re-download while developing parsers.
  • Idempotent writes — same URL scraped twice shouldn’t duplicate truth without a plan.

Level 3 — JavaScript-heavy pages

When the HTML shell is empty and data appears only after JS:

ToolWhen
PlaywrightModern default for headless browsers
SeleniumLegacy ecosystem still common

Pattern: navigate → wait for selector → page.content() → parse as static HTML, or intercept JSON responses and skip the DOM.

Browser automation is slower and louder. Use it when Level 1 fails, not as ego.

Level 4 — Frameworks for many spiders

Scrapy (and similar) when you need:

  • concurrent requests with politeness extensions
  • item pipelines
  • scheduled crawls
  • standardized middleware

Don’t start here for a five-page project. Do graduate here when you have a fleet of sources.

Level 5 — Hardening the craft

  • Type rows with Pydantic; reject garbage early.
  • Log url, status, byte_length, parse_count.
  • Snapshot raw HTML on failure for replay.
  • Pin dependencies; run parsers in CI against fixture HTML (vendored samples you are allowed to store).
  • Separate fetch from parse modules so anti-bot changes don’t rewrite business logic.

Part IV — Core techniques (cheat sheet)

TechniqueIntent
CSS / XPath selectorsLocate nodes without string chaos
urljoinCorrect relative links
Session / cookiesMulti-step flows you are allowed to use
Backoff on 429 / 503Survive rate limits
Robots + crawl-delayPolicy before code
Sitemap.xmlDiscovery without guessing URLs
Content-hash (SHA256 of normalized row)Change detection
Headless only when neededCost and ban surface
Extract JSON-LD / microdataStructured data sites already embed
Validate UTF-8 / fix mojibakeFinance and résumés hate wrong glyphs

First principles of a resilient scrape:

  Identify → May I?
  Discover → What URLs?
  Fetch    → How often, with what identity?
  Parse    → What is the schema?
  Prove    → Fixtures + counts + hashes
  Store    → Where does truth live?
  Diff     → What changed since last run?
  Act      → Alert, chart, apply, stop

Part V — What to do after you scrape (where leverage actually lands)

Fetching is 20%. The rest is career/finance compound interest.

1. Clean

  • Trim, normalize whitespace, unify dates (ISO-8601), money as decimals, URLs absolute.
  • Deduplicate on natural keys (url, sku, job_id).
  • Drop rows missing the key that makes the row useful.

2. Store (pick boring)

ScaleStore
Learning / personalCSV + Git-LFS or just CSV
Ongoing personal systemsSQLite (one file, SQL, backups)
AnalyticsParquet + DuckDB / pandas
Multi-devicePostgres when you outgrow SQLite

Schema example (jobs):

jobs(
  id, source, external_id, title, company, location,
  url, first_seen, last_seen, raw_hash, payload_json
)

3. Validate

  • Row count floors/ceilings (0 rows = alarm, 10× yesterday = alarm).
  • Required fields non-null rate.
  • Sample manual review weekly—automation drifts.

4. Transform into decisions

CareerFinance
Frequency of skills in JDs over timePrice series → median / volatility
New companies hiring your stackAlert if price < threshold
Geo / remote ratiosBasket cost of a shopping list

Use charts, not vibes. Even a tiny notebook beats a stack of bookmarks.

5. Automate the schedule

  • cron, systemd timer, or CI scheduled workflow.
  • Alert on failure (email, Telegram, whatever you already use).
  • Keep secrets (if any) out of the repo.

6. Monitor change (the secret weapon)

Most leverage is diff:

  • New job matching keywords.
  • Price drop.
  • Changelog line containing security or breaking.
  • Status page not operational.

Store yesterday’s hash set; emit only deltas. Humans drown in full dumps; they act on events.

7. Optional: LLM after structure

Once rows are clean, models can classify JDs, tag skills, or summarize a single posting. Do not use an LLM as your HTML parser of first resort for production tables—use parsers; use models for language on top of schema (and schema-bound extraction when HTML is hopeless, with validation).


Part VI — Failure modes (how leverage dies)

FailureSymptomFix
Brittle selectorsEmpty data after redesignFixtures + multiple strategies + tests
Ban / CAPTCHA403, endless challengesSlow down, API, permission, different source
Legal / ToSLetter from counselStop; get rights; use official data
PII sprawlYou store emails you shouldn’tMinimize fields; retention policy
HoardingTB of junk HTMLScrape decisions, not the whole internet
No schemaNotebook chaosPydantic + SQLite from day one
Silent driftCharts lie for monthsAlerts on count/hash anomalies

A minimal “career + finance” starter lab (ethical sandbox)

  1. Own site or local static HTML fixtures — learn selectors safely.
  2. One public status page or docs changelog that allows bots.
  3. Pipeline: fetch → parse → SQLite → daily timer → “new rows” printout.
  4. Only then point at higher-stakes sources with policy read first.

If you want excitement without gray area: scrape your deployment docs, your portfolio, your lab services’ pages—and wire the same pipeline you would use on the wild web.


Closing map

  WHY          leverage = lower search/forget cost + owned time series
  MODEL        web = messy DB; guest etiquette; pipeline > dump
  WHAT         decision-relevant public/permitted surfaces
  HOW          API first → httpx+parser → browser → framework
  AFTER        clean → store → validate → diff → alert → decide
  NOT          hoover everything, ignore robots, skip schema

Web scraping is high-leverage because it turns attention into assets: tables you can query when a hiring wave hits, when a price moves, when a dependency blips. It is exciting because every broken page is a puzzle at the intersection of HTTP, documents, and your real life.

Do it like an engineer who will still respect the network tomorrow—and the leverage compounds.


Next pull on the thread: Once the data is yours, how do you design change alerts that don’t spam you into ignoring them—and which metrics deserve a systemd timer vs a manual run?

Back to notes