正在学习

5.5 Example Project: Optimizing a Web Scraper

Requirements:

pip install requests beautifulsoup4

import time

from typing import List, Dict

import requests

from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "MasteringClaudeCode/1.0 (+https://example.org)"}

def fetch(url: str, timeout: float = 10.0) -> str:

"""Fetch a URL synchronously and return text content."""

resp = requests.get(url, headers=HEADERS, timeout=timeout)

resp.raise_for_status()

return resp.text

def parse(html: str) -> Dict[str, str]:

"""Extract simple fields: <title> and first <h1> if present."""

soup = BeautifulSoup(html, "html.parser")

title = (soup.title.string or "").strip() if soup.title else ""

h1 = (soup.find("h1").get_text(strip=True)) if soup.find("h1") else ""

return {"title": title, "h1": h1}

def scrape(urls: List[str]) -> List[Dict[str, str]]:

results = []

for url in urls:

    html = fetch(url)

    data = parse(html)

    data["url"] = url

    results.append(data)

    time.sleep(0.2) # polite delay

return results

if name == "main":

URLS = [

    "https://example.com",

    "https://httpbin.org/html",

    "https://www.iana.org/domains/reserved",

]

start = time.time()

items = scrape(URLS)

elapsed = time.time() - start

for item in items:

    print(f"{item['url']}\n title: {item['title']}\n h1: {item['h1']}\n")

print(f"Baseline elapsed: {elapsed:.3f}s")

This baseline is simple and correct, but it blocks on each network request. As the URL list grows, total time scales linearly with latency, and any transient error stops progress.

Optimized: Concurrent, Pooled, and Robust Scraper

```python

# optimized_scraper.py

# Requirements:

# pip install aiohttp aiodns beautifulsoup4 cchardet orjson

# (aiodns optional; speeds up DNS on some platforms)
import asyncio

from typing import List, Dict, Optional, Tuple

from contextlib import asynccontextmanager

import random

import time

import aiohttp

from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "MasteringClaudeCode/1.0 (+https://example.org)"}

Bounded concurrency avoids overwhelming remote servers and your machine.

MAX_CONCURRENCY = 10

REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=12, connect=5, sock_connect=5, sock_read=10)

@asynccontextmanager

async def make_session() -> aiohttp.ClientSession:

connector = aiohttp.TCPConnector(limit=MAX_CONCURRENCY, ttl_dns_cache=300)

async with aiohttp.ClientSession(headers=HEADERS, timeout=REQUEST_TIMEOUT, connector=connector) as session:

yield session

async def fetch(session: aiohttp.ClientSession, url: str, attempt: int = 1, max_attempts: int = 3) -> str:

"""

Fetch a URL with exponential backoff on transient errors.

Raises the last exception if all attempts fail.

"""

try:

async with session.get(url) as resp:

Fail fast on non-2xx responses

if resp.status >= 400:

raise aiohttp.ClientResponseError(

request_info=resp.request_info,

history=resp.history,

status=resp.status,

message=f"HTTP {resp.status}",

headers=resp.headers,

)

Decode efficiently; rely on aiohttp's chardet if needed

return await resp.text()

except (aiohttp.ClientError, asyncio.TimeoutError) as e:

if attempt >= max_attempts:

raise

Exponential backoff with jitter

backoff = (2 ** (attempt - 1)) + random.uniform(0, 0.25)

await asyncio.sleep(backoff)

return await fetch(session, url, attempt + 1, max_attempts)

def parse_fast(html: str) -> Dict[str, str]:

"""

Parse using 'lxml' if available via BeautifulSoup for speed,

falling back to 'html.parser' otherwise.

"""

try:

soup = BeautifulSoup(html, "lxml")

except Exception:

soup = BeautifulSoup(html, "html.parser")

title = (soup.title.string or "").strip() if soup.title else ""

h1_tag = soup.find("h1")

h1 = h1_tag.get_text(strip=True) if h1_tag else ""

return {"title": title, "h1": h1}

async def scrape_one(semaphore: asyncio.Semaphore, session: aiohttp.ClientSession, url: str) -> Tuple[str, Optional[Dict[str, str]], Optional[str]]:

async with semaphore:

try:

html = await fetch(session, url)

data = parse_fast(html)

data["url"] = url

return url, data, None

except Exception as e:

return url, None, f"{type(e).name}: {e}"

async def scrape(urls: List[str]) -> List[Dict[str, str]]:

semaphore = asyncio.Semaphore(MAX_CONCURRENCY)

async with make_session() as session:

tasks = [scrape_one(semaphore, session, url) for url in urls]

results = []

for coro in asyncio.as_completed(tasks):

url, data, err = await coro

if err:

results.append({"url": url, "error": err, "title": "", "h1": ""})

else:

results.append(data)

return results

if name == "main":

URLS = [

"https://example.com",

"https://httpbin.org/html",

"https://www.iana.org/domains/reserved",

Add more URLs here to see concurrency shine

]

started = time.time()

items = asyncio.run(scrape(URLS))

elapsed = time.time() - started

for item in items:

if item.get("error"):

print(f"{item['url']}\n error: {item['error']}\n")

else:

print(f"{item['url']}\n title: {item['title']}\n h1: {item['h1']}\n")

print(f"Optimized elapsed: {elapsed:.3f}s with concurrency={MAX_CONCURRENCY}")

Key improvements in the optimized version:

  • Bounded concurrency with a semaphore and a connector limit ensures politeness and protects resources.
  • Connection pooling via TCPConnector reduces handshake overhead across many requests.
  • Timeouts and backoff prevent stuck requests and recover from transient failures without crashing the entire run.
  • Faster parsing prefers lxml automatically while remaining compatible with the standard parser.
  • Structured results capture successes and errors uniformly, keeping downstream processing simple.

Clarification Table

Optimization What Changed Why It Matters Impact on Behavior
Concurrency Single-thread loop → asyncio with aiohttp and semaphore Hides network latency by overlapping I/O Dramatically lowers wall-clock time for many URLs
Connection Reuse One TCP connection per request → pooled TCPConnector Reduces repeated handshakes and DNS lookups Cuts per-request overhead
Timeouts & Backoff No timeouts/retries → explicit timeouts + exponential backoff Fails fast and recovers from transient errors Improves robustness and throughput stability
Parser Choice Built-in parser only → prefer lxml with fallback Faster HTML parsing in CPython Reduces CPU time per page
Error Handling Exceptions bubble → structured error rows Keeps the pipeline running on partial failures Better operability at scale

You started with a correct synchronous scraper and, by reasoning about where time is truly spent, evolved it into a concurrent, pooled, and fault-tolerant pipeline that remains easy to read and modify. Claude Code’s value in this workflow is the ability to explain why each change matters, predict its impact, and keep the design clean as performance improves. In the next section, you will generalize these techniques into a reusable optimization checklist for data-collection services: measuring bottlenecks, choosing the right concurrency model, and validating that improved speed doesn’t compromise correctness or maintainability.

练习题

What is a key characteristic of the baseline scraper?

A. It uses concurrent requests to minimize latency
B. It blocks on each network request
C. It automatically retries transient errors
D. It uses connection pooling for efficiency

Which package is NOT required for the optimized scraper?

A. aiohttp
B. beautifulsoup4
C. requests
D. orjson

What are the benefits of bounded concurrency in the optimized scraper? (Select all that apply)

A. Prevents overwhelming remote servers
B. Reduces network latency
C. Protects local system resources
D. Eliminates the need for timeouts

The optimized scraper uses exponential backoff to handle transient errors.

The optimized scraper sets the total request timeout to ___ seconds.

Explain the purpose of the parse_fast function in the optimized scraper.

Which of the following is a key improvement in the optimized scraper compared to the baseline?

A. Simpler error handling
B. Bounded concurrency with a semaphore
C. Synchronous request processing
D. No timeouts for requests

What are the components of the REQUEST_TIMEOUT in the optimized scraper? (Select all that apply)

A. total=12
B. connect=5
C. sock_connect=5
D. sock_read=10
E. retry=3

The optimized scraper uses a TCPConnector with a limit to manage connection pooling.

The optimized scraper uses ___ to yield results as they are processed, rather than storing them all in memory.

Describe how the optimized scraper handles transient errors during network requests.

Which of the following are structured results captured by the optimized scraper? (Select all that apply)

A. Successes with data
B. Errors with descriptions
C. Parsing times
D. URLs processed

The optimized scraper's parse_fast function always uses 'lxml' for parsing.

The optimized scraper introduces a ___ to limit the number of concurrent requests.

Why does the optimized scraper use structured results instead of simply raising exceptions for errors?

What is the primary benefit of using bounded concurrency in a web scraper?

A. It increases the number of requests per second
B. It prevents overwhelming remote servers and your own machine
C. It eliminates the need for timeouts
D. It makes the code more readable

Which of the following are key improvements in the optimized scraper compared to the baseline?

A. Bounded concurrency with a semaphore and a connector limit
B. Use of list comprehensions for parsing
C. Timeouts and backoff for handling transient errors
D. Faster parsing with lxml
E. Synchronous requests for simplicity

The optimized scraper uses exponential backoff to handle transient errors, which helps prevent crashing the entire run.

The optimized scraper prefers ___ for parsing HTML when available, falling back to 'html.parser' otherwise, to improve parsing speed.

Explain why bounded concurrency is important in web scraping and how it relates to the concept of politeness in scraping.

登录后解锁笔记、知识点解析、AI 问答

立即登录