正在学习
5.5 示例项目:优化网络爬虫
# 依赖:
# 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:
"""同步获取一个 URL 并返回文本内容。"""
resp = requests.get(url, headers=HEADERS, timeout=timeout)
resp.raise_for_status()
return resp.text
def parse(html: str) -> Dict[str, str]:
"""提取简单字段:<title> 和第一个 <h1>(如果存在)。"""
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) # 礼貌性延迟
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")
这个基础版本简单且正确,但它在每次网络请求时都会阻塞。随着 URL 列表的增长,总耗时与延迟呈线性关系,并且任何瞬时错误都会中断处理。
优化版:并发、连接池化且健壮的爬虫
# optimized_scraper.py
# 依赖:
# pip install aiohttp aiodns beautifulsoup4 cchardet orjson
# (aiodns 为可选;在某些平台上可加速 DNS 解析)
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)"}
# 有界并发可避免压垮远端服务器和本地机器。
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:
"""
获取一个 URL,在瞬时错误时采用指数退避。
若所有尝试都失败,则抛出最后一个异常。
"""
try:
async with session.get(url) as resp:
# 对非 2xx 响应快速失败
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,
)
# 高效解码;必要时依赖 aiohttp 的 chardet
return await resp.text()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
if attempt >= max_attempts:
raise
# 带抖动的指数退避
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]:
"""
优先使用 BeautifulSoup 的 'lxml' 解析器以提升速度,
不可用时回退到 'html.parser'。
"""
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",
# 在此处添加更多 URL 以观察并发效果
]
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}")
优化版的关键改进:
- 通过信号量和连接器限制实现有界并发,既保持礼貌访问,又保护资源。
- 通过 TCPConnector 实现连接池化,减少大量请求间的握手开销。
- 超时与退避机制可避免请求卡死,并能从容应对瞬时失败,而不会让整个流程崩溃。
- 更快的解析会自动优先选用 lxml,同时保持对标准解析器的兼容性。
- 结构化的结果统一捕获成功与错误,使下游处理更简单。
| 优化项 | 变更内容 | 重要性 | 对行为的影响 |
|---|---|---|---|
| 并发 | 单线程循环 → 使用 aiohttp 和信号量的 asyncio | 通过 I/O 重叠隐藏网络延迟 | 显著降低多个 URL 的墙钟时间 |
| 连接复用 | 每个请求一个 TCP 连接 → 池化 TCPConnector | 减少重复的握手和 DNS 查询 | 降低每个请求的开销 |
| 超时与退避 | 无超时/重试 → 显式超时 + 指数退避 | 快速失败并从瞬态错误中恢复 | 提高鲁棒性和吞吐量稳定性 |
| 解析器选择 | 仅使用内置解析器 → 优先使用 lxml 并设置回退 | 在 CPython 中实现更快的 HTML 解析 | 减少每页的 CPU 时间 |
| 错误处理 | 异常向上抛出 → 结构化错误行 | 在部分失败时保持管道运行 | 提升大规模场景下的可操作性 |
你从一个正确的同步爬虫出发,通过深入思考时间究竟消耗在哪里,逐步将其演进为一个并发、连接池化且具备容错能力的管道,同时保持了良好的可读性和可修改性。Claude Code 在此工作流中的价值在于:能够解释每次变更为何重要、预测其影响,并在性能提升的同时保持设计的整洁性。在下一节中,你将把这些技术推广为数据采集服务可复用的优化清单:测量瓶颈、选择合适的并发模型,并验证提速不以牺牲正确性或可维护性为代价。
练习题
基线爬虫的一个关键特征是什么?
A. 它使用并发请求以最小化延迟
B. 它会在每次网络请求时阻塞
C. 它会自动重试临时性错误
D. 它使用连接池以提高效率
优化版爬虫不需要哪个包?
A. aiohttp
B. beautifulsoup4
C. requests
D. orjson
优化后的爬虫中,有界并发有哪些优势?(选择所有适用的项)
A. 防止压垮远程服务器
B. 降低网络延迟
C. 保护本地系统资源
D. 消除对超时的需求
优化后的爬虫使用指数退避来处理瞬时错误。
优化后的爬虫将总请求超时设置为___秒。
解释优化爬虫中 parse_fast 函数的目的。
与基线相比,优化后的爬虫的以下哪项是一项关键改进?
A. 更简单的错误处理
B. 使用信号量的有界并发
C. 同步请求处理
D. 请求没有超时
优化后的爬虫中 REQUEST_TIMEOUT 的组成部分是什么?(选择所有适用的)
A. total=12
B. connect=5
C. sock_connect=5
D. sock_read=10
E. retry=3
优化后的爬虫使用带有连接数限制的 TCPConnector 来管理连接池。
优化后的爬虫使用 ___ 在结果处理完成时立即产出,而不是将它们全部存储在内存中。
描述优化后的爬虫在网络请求期间如何处理瞬态错误。
以下哪些是由优化后的爬虫所捕获的结构化结果?(选择所有适用的)
A. 带有数据的成功项
B. 带有描述的错误项
C. 解析时间
D. 已处理的 URL
优化后的爬虫的 parse_fast 函数总是使用 'lxml' 进行解析。
优化后的爬虫引入了一个___来限制并发请求的数量。
为什么优化后的爬虫使用结构化结果,而不是简单地通过抛出异常来处理错误?
在网页爬虫中使用有界并发的最大好处是什么?
A. 它增加了每秒请求数
B. 它防止压垮远程服务器和您自己的机器
C. 它消除了对超时的需求
D. 它使代码更易读
与基线相比,优化后的爬虫在以下哪些方面进行了关键改进?
A. 使用信号量和连接器限制的有界并发
B. 使用列表推导式进行解析
C. 用于处理瞬时错误的超时和退避机制
D. 使用 lxml 进行更快的解析
E. 为简单起见使用同步请求
优化后的爬虫使用指数退避来处理瞬时错误,这有助于防止整个运行崩溃。
优化后的爬虫在可用时优先使用 ___ 解析 HTML,否则回退到 'html.parser',以提高解析速度。
解释为什么有界并发在网页爬取中很重要,以及它与爬取中礼貌性概念的关系。
登录后解锁笔记、知识点解析、AI 问答
立即登录