Lab 09 — Async Web Scraper with Generators
Hands-on Lab 60 min Advanced
Objectives
- Write generator functions for URL generation
- Use
asyncio.gatherfor concurrent HTTP checks - Use
itertoolsfor batching requests - Process results with generator expressions
- Compare sync vs async performance
Step 1 — Install Dependencies
mkdir ~/python-course/lab-09 && cd ~/python-course/lab-09
python3 -m venv venv && source venv/bin/activate
pip install aiohttp
Step 2 — Build the Scraper
Create url_checker.py:
#!/usr/bin/env python3
"""
Lab 09 — Async URL Checker
Uses generators, asyncio, and itertools.
"""
import asyncio
import itertools
import time
from collections import Counter
URLS = [
"https://python.org",
"https://docs.python.org",
"https://pypi.org",
"https://flask.palletsprojects.com",
"https://pandas.pydata.org",
"https://numpy.org",
"https://github.com",
"https://example.com",
"https://httpbin.org/status/404",
"https://httpbin.org/status/500",
]
# ── Generator Functions ────────────────────────────────────────
def url_batches(urls, batch_size=3):
"""Yield URLs in batches using itertools."""
for batch in itertools.batched(urls, batch_size):
yield list(batch)
def parse_status(results):
"""Generator that yields (url, status, category) tuples."""
for url, status, elapsed in results:
if status is None:
category = "ERROR"
elif status < 300:
category = "OK"
elif status < 400:
category = "REDIRECT"
elif status < 500:
category = "CLIENT_ERROR"
else:
category = "SERVER_ERROR"
yield url, status, elapsed, category
# ── Async HTTP ────────────────────────────────────────────────
async def check_url(session, url):
"""Check a single URL and return (url, status_code, elapsed)."""
import aiohttp
start = time.perf_counter()
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
elapsed = time.perf_counter() - start
return url, resp.status, round(elapsed, 3)
except Exception as e:
elapsed = time.perf_counter() - start
return url, None, round(elapsed, 3)
async def check_batch(urls):
"""Check a batch of URLs concurrently."""
import aiohttp
async with aiohttp.ClientSession() as session:
tasks = [check_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
async def check_all_async(urls, batch_size=3):
"""Check all URLs in batches."""
all_results = []
for i, batch in enumerate(url_batches(urls, batch_size), 1):
print(f" Checking batch {i}: {len(batch)} URLs...")
results = await check_batch(batch)
all_results.extend(results)
return all_results
# ── Report ─────────────────────────────────────────────────────
def print_report(results):
parsed = list(parse_status(results))
categories = Counter(cat for _, _, _, cat in parsed)
print(f"\n{'='*65}")
print(f" {'URL':<40} {'STATUS':>8} {'TIME':>8} CAT")
print(f"{'─'*65}")
for url, status, elapsed, cat in sorted(parsed, key=lambda x: x[3]):
status_str = str(status) if status else "ERR"
print(f" {url[:40]:<40} {status_str:>8} {elapsed:>7.3f}s {cat}")
print(f"{'='*65}")
print(f"\n Summary:")
for cat, count in sorted(categories.items()):
bar = "█" * count
print(f" {cat:<15} {bar} ({count})")
async def main():
print("🔍 Async URL Checker — Lab 09\n")
start = time.perf_counter()
results = await check_all_async(URLS, batch_size=3)
total = time.perf_counter() - start
print_report(results)
print(f"\n ⏱ Total time: {total:.2f}s for {len(URLS)} URLs\n")
if __name__ == "__main__":
asyncio.run(main())
Step 3 — Run
python3 url_checker.py
Verification ✅
The 10 URLs should complete in ~3-5s (batches of 3 in parallel), not 10× single URL time.
Summary
url_batches()generator usesitertools.batchedto yield URL groupsparse_status()generator transforms raw results without storing all in memoryasyncio.gather()runs all URLs in a batch concurrentlycheck_all_async()processes batches sequentially but each batch is concurrent- Generator pipeline:
url_batches → check_batch → parse_status → report