Focused practice · 9 min read

Python Web Scraping Exercises: Build a Polite Pipeline

A scraper is a pipeline, not a single request. These exercises separate fetching, parsing, extracting, and storing so every stage can be tested without hitting a live site.

Start from a saved page

Download once during development, save the HTML, and parse the fixture repeatedly. This removes network variance from your debugging and lets you test edge cases such as missing attributes or repeated links.

from html.parser import HTMLParser

class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []
    def handle_starttag(self, tag, attrs):
        href = dict(attrs).get("href")
        if tag == "a" and href:
            self.links.append(href)

p = Links()
p.feed('<a href="/a">A</a><a href="/a">A</a><a href="/b">B</a>')
print(p.links)

A fixture you can rerun

Build the request separately

Keep request construction in one function. Set a descriptive User-Agent and a timeout when you eventually send it. The browser runner on this site does not send network requests, so these examples stop at the Request object.

from urllib.request import Request

request = Request(
    "https://example.com/docs",
    headers={"User-Agent": "LearningBot/1.0"},
)
print(request.full_url)
print(request.get_header("User-agent"))

No network in this example

Branch on the status

Do not parse a 404 page as if it were the page you wanted. A small status function keeps the decision visible, and it gives the retry policy one place to live.

def outcome(status):
    if 200 <= status < 300:
        return "parse"
    if 300 <= status < 400:
        return "follow redirect"
    if status == 429:
        return "back off"
    return "stop"

print(outcome(200))
print(outcome(429))

A small status policy

Store compact records

Extract the fields the task needs and leave the rest behind. Use a list of dictionaries for structured output, and deduplicate by a stable key such as the absolute URL. This keeps the result inspectable before it reaches a file or database.

rows = [
    {"url": "/a", "title": "A"},
    {"url": "/a", "title": "A"},
    {"url": "/b", "title": "B"},
]
unique = list({row["url"]: row for row in rows}.values())
print(unique)

One record per URL

Read the rules before the crawl

robots.txt is a starting point, not the whole agreement. Read the site terms, identify your client, limit request rate, and avoid personal data unless you have a lawful basis. If a site says no, stop.

robots = "User-agent: *\nDisallow: /private\nCrawl-delay: 2"
rules = {}
for line in robots.splitlines():
    if ":" in line:
        key, value = line.split(":", 1)
        rules[key.strip().lower()] = value.strip()
print(rules["disallow"])
print(rules["crawl-delay"])

The smallest useful check

Your practice checklist

Common questions

Can the browser runner fetch a real website?

No. The lesson runner is for local Python examples and does not send arbitrary network requests. Run real fetches in a controlled Python environment.

Should I use requests or urllib?

Both are valid. urllib is in the standard library; requests is a popular third-party convenience. The parsing and ethics rules are the same.

What should I do when a site blocks me?

Stop. Do not bypass logins, CAPTCHAs, paywalls, or access controls. Look for an API or ask for permission instead.

Practice the scraping pipeline

12 questions on requests, HTML parsing, deduplication, and polite crawl limits.

Open practice

Keep reading: