🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [📌 上期回顾](#📌 上期回顾)
- [🎯 本节目标](#🎯 本节目标)
- 一、超时设置:避免程序"卡死"
-
- [1.1 为什么必须设置超时?](#1.1 为什么必须设置超时?)
- [1.2 超时参数详解](#1.2 超时参数详解)
- [1.3 捕获超时异常](#1.3 捕获超时异常)
- 二、重试机制:提高成功率
-
- [2.1 为什么需要重试?](#2.1 为什么需要重试?)
- [2.2 简单重试(固定延迟)](#2.2 简单重试(固定延迟))
- [2.3 指数退避算法(推荐)](#2.3 指数退避算法(推荐))
- 三、错误分类与处理策略
-
- [3.1 错误类型判断](#3.1 错误类型判断)
- [3.2 智能重试实现](#3.2 智能重试实现)
- [四、生产级 HttpClient(完整版)](#四、生产级 HttpClient(完整版))
-
- [4.1 设计要求](#4.1 设计要求)
- [4.2 完整实现](#4.2 完整实现)
- 五、失败日志设计
-
- [5.1 日志级别规范](#5.1 日志级别规范)
- [5.2 结构化日志](#5.2 结构化日志)
- 六、本节小结
- 七、课后作业(必做,验收进入下一节)
-
- [任务1:测试 HttpClient](#任务1:测试 HttpClient)
- 任务2:扩展功能
- 任务3:真实场景压测
- 🔮下期预告
- 🌟文末
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》
订阅后更新会优先推送,按目录学习更高效~
📌 上期回顾
在上一节《伪装与会话:Headers、Session、Cookie(合规使用)!》中,我们学习了如何使用 Session 管理会话状态,以及如何设置合适的请求头来伪装爬虫。你已经能够处理需要 Cookie 的网站了。
但是,网络世界并不完美:服务器可能宕机、网络可能抖动、响应可能超时...如果不做好异常处理,你的爬虫会频繁崩溃。
这一节,我们将学习生产级爬虫的稳定性设计,让你的爬虫能够 7×24 小时稳定运行!
🎯 本节目标
通过本节学习,你将能够:
- 理解超时设置的重要性和最佳实践
- 实现智能重试机制(指数退避算法)
- 区分可重试错误和不可重试错误
- 设计失败日志和监控指标
- 编写生产级 HttpClient
- 交付验收:编写一个能自动重试的请求器,并测试其稳定性
一、超时设置:避免程序"卡死"
1.1 为什么必须设置超时?
问题场景:
python
# ❌ 没有设置超时
response = requests.get(url)
# 如果服务器不响应,程序会永远等待下去...
# 你的爬虫任务会卡在这里,无法继续
真实案例:
- 服务器负载过高,响应缓慢
- 网络链路故障,数据包丢失
- 目标网站被 DDoS 攻击,无法响应
后果:
- ❌ 程序假死,无法继续执行
- ❌ 资源占用无法释放
- ❌ 定时任务超时失败
1.2 超时参数详解
python
import requests
# timeout 可以是单个值(总超时)
response = requests.get(url, timeout=10) # 10秒内必须完成
# timeout 也可以是元组(连接超时, 读取超时)
response = requests.get(url, timeout=(3, 10))
# 3秒内建立连接,10秒内读取完响应
推荐配置:
python
# 快速接口(如 API)
timeout = (3, 10)
# 普通网页
timeout = (5, 15)
# 大文件下载
timeout = (5, 60)
# 慢速网站
timeout = (10, 30)
1.3 捕获超时异常
python
import requests
from requests.exceptions import Timeout, ConnectTimeout, ReadTimeout
def fetch_with_timeout(url):
"""带超时处理的请求"""
try:
response = requests.get(url, timeout=(5, 15))
return response
except ConnectTimeout:
print(f"❌ 连接超时: {url}")
return None
except ReadTimeout:
print(f"❌ 读取超时: {url}")
return None
except Timeout:
print(f"❌ 请求超时: {url}")
return None
二、重试机制:提高成功率
2.1 为什么需要重试?
临时性错误(应该重试):
- ✅ 网络抖动(ConnectionError)
- ✅ 服务器暂时过载(503 Service Unavailable)
- ✅ 读取超时(ReadTimeout)
- ✅ DNS 解析失败(临时)
永久性错误(不应重试):
- ❌ 404 Not Found(页面不存在)
- ❌ 403 Forbidden(无权限)
- ❌ 401 Unauthorized(未登录)
- ❌ 400 Bad Request(请求参数错误)
2.2 简单重试(固定延迟)
python
import time
def fetch_with_retry(url, max_retries=3, delay=2):
"""
带固定延迟的重试
Args:
url: 目标URL
max_retries: 最大重试次数
delay: 重试间隔(秒)
"""
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # 4xx/5xx 抛出异常
print(f"✅ 成功(尝试 {attempt+1}/{max_retries})")
return response
except requests.exceptions.RequestException as e:
print(f"❌ 失败(尝试 {attempt+1}/{max_retries}): {e}")
if attempt < max_retries - 1:
print(f"⏳ 等待 {delay} 秒后重试...")
time.sleep(delay)
else:
print(f"💔 放弃请求: {url}")
return None
2.3 指数退避算法(推荐)
核心思想:重试间隔逐步增加,避免瞬时压力
json
第1次失败 → 等待 2 秒
第2次失败 → 等待 4 秒
第3次失败 → 等待 8 秒
第4次失败 → 等待 16 秒
实现代码:
python
import time
import random
def fetch_with_backoff(url, max_retries=5, base_delay=2, max_delay=60):
"""
带指数退避的重试
Args:
url: 目标URL
max_retries: 最大重试次数
base_delay: 基础延迟(秒)
max_delay: 最大延迟(秒)
"""
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
print(f"✅ 成功(尝试 {attempt+1})")
return response
except requests.exceptions.RequestException as e:
print(f"❌ 失败(尝试 {attempt+1}/{max_retries}): {e}")
if attempt < max_retries - 1:
# 计算退避时间:2^attempt * base_delay
delay = min(base_delay * (2 ** attempt), max_delay)
# 加入随机抖动(避免"惊群效应")
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"⏳ 等待 {wait_time:.2f} 秒后重试...")
time.sleep(wait_time)
else:
print(f"💔 放弃请求: {url}")
return None
优势对比:
| 策略 | 第1次 | 第2次 | 第3次 | 第4次 | 总耗时 |
|---|---|---|---|---|---|
| 固定延迟(2s) | 2s | 2s | 2s | 2s | 8s |
| 指数退避(2s) | 2s | 4s | 8s | 16s | 30s |
适用场景:
- 固定延迟:网络稳定,偶尔抖动
- 指数退避:服务器过载,需要缓冲时间
三、错误分类与处理策略
3.1 错误类型判断
python
from requests.exceptions import (
RequestException,
ConnectionError,
Timeout,
HTTPError,
TooManyRedirects
)
class ErrorClassifier:
"""错误分类器"""
# 可重试的 HTTP 状态码
RETRYABLE_STATUS_CODES = {
408, # Request Timeout
429, # Too Many Requests
500, # Internal Server Error
502, # Bad Gateway
503, # Service Unavailable
504, # Gateway Timeout
}
# 不可重试的状态码
NON_RETRYABLE_STATUS_CODES = {
400, # Bad Request
401, # Unauthorized
403, # Forbidden
404, # Not Found
405, # Method Not Allowed
410, # Gone
}
@classmethod
def should_retry(cls, exception):
"""
判断错误是否应该重试
Args:
exception: 异常对象
Returns:
bool: 是否应该重试
"""
# 1. 网络相关错误 → 重试
if isinstance(exception, (ConnectionError, Timeout)):
return True
# 2. HTTP 错误
if isinstance(exception, HTTPError):
response = exception.response
if response is not None:
status_code = response.status_code
# 明确可重试的状态码
if status_code in cls.RETRYABLE_STATUS_CODES:
return True
# 明确不可重试的状态码
if status_code in cls.NON_RETRYABLE_STATUS_CODES:
return False
# 3. 重定向过多 → 不重试
if isinstance(exception, TooManyRedirects):
return False
# 4. 其他未知错误 → 默认重试一次
return True
# 使用示例
try:
response = requests.get(url)
response.raise_for_status()
except RequestException as e:
if ErrorClassifier.should_retry(e):
print("✅ 可重试")
else:
print("❌ 不应重试")
3.2 智能重试实现
python
def smart_fetch(url, max_retries=5):
"""
智能重试请求
- 根据错误类型决定是否重试
- 使用指数退避算法
"""
base_delay = 2
max_delay = 60
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=(5, 15))
response.raise_for_status()
print(f"✅ 请求成功")
return response
except RequestException as e:
print(f"❌ 失败(尝试 {attempt+1}/{max_retries})")
print(f" 错误类型: {type(e).__name__}")
print(f" 错误信息: {e}")
# 判断是否应该重试
if not ErrorClassifier.should_retry(e):
print(f"💔 错误不可重试,放弃")
return None
# 如果还有重试机会
if attempt < max_retries - 1:
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"⏳ {wait_time:.2f} 秒后重试...")
time.sleep(wait_time)
else:
print(f"💔 达到最大重试次数,放弃")
return None
return None
四、生产级 HttpClient(完整版)
4.1 设计要求
✅ 支持超时设置
✅ 支持智能重试(指数退避)
✅ 支持 Session 管理
✅ 支持失败日志记录
✅ 支持请求统计(成功率、平均耗时)
✅ 代码可复用
4.2 完整实现
python
"""
生产级 HTTP 客户端 v1.0
文件名: http_client.py
"""
import requests
import time
import random
import logging
from typing import Optional, Dict, Any
from datetime import datetime
from requests.exceptions import RequestException, HTTPError
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
class HttpClient:
"""生产级 HTTP 客户端"""
# 可重试的状态码
RETRYABLE_CODES = {408, 429, 500, 502, 503, 504}
def __init__(self,
max_retries: int = 5,
base_delay: float = 2.0,
max_delay: float = 60.0,
timeout: tuple = (5, 15)):
"""
初始化客户端
Args:
max_retries: 最大重试次数
base_delay: 基础延迟(秒)
max_delay: 最大延迟(秒)
timeout: 超时设置 (连接超时, 读取超时)
"""
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.timeout = timeout
# 创建 Session
self.session = requests.Session()
# 设置默认请求头
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
})
# 统计信息
self.stats = {
'total_requests': 0,
'successful_requests': 0,
'failed_requests': 0,
'total_retries': 0,
'total_time': 0.0
}
def get(self, url: str, **kwargs) -> Optional[requests.Response]:
"""GET 请求"""
return self._request('GET', url, **kwargs)
def post(self, url: str, **kwargs) -> Optional[requests.Response]:
"""POST 请求"""
return self._request('POST', url, **kwargs)
def _request(self,
method: str,
url: str,
**kwargs) -> Optional[requests.Response]:
"""
核心请求方法(带重试)
Args:
method: 请求方法 (GET/POST)
url: 目标URL
**kwargs: 传递给 requests 的参数
Returns:
Response 对象,失败返回 None
"""
self.stats['total_requests'] += 1
start_time = time.time()
# 合并超时设置
if 'timeout' not in kwargs:
kwargs['timeout'] = self.timeout
for attempt in range(self.max_retries):
try:
# 发起请求
if method == 'GET':
response = self.session.get(url, **kwargs)
else:
response = self.session.post(url, **kwargs)
# 检查状态码
response.raise_for_status()
# 成功
elapsed = time.time() - start_time
self.stats['successful_requests'] += 1
self.stats['total_time'] += elapsed
if attempt > 0:
self.stats['total_retries'] += attempt
logger.info(f"✅ 请求成功(重试 {attempt} 次): {url}")
return response
except RequestException as e:
elapsed = time.time() - start_time
# 记录日志
logger.warning(
f"❌ 请求失败 (尝试 {attempt+1}/{self.max_retries}): {url}\n"
f" 错误: {type(e).__name__}: {e}"
)
# 判断是否应该重试
should_retry = self._should_retry(e)
if not should_retry:
logger.error(f"💔 错误不可重试,放弃: {url}")
self.stats['failed_requests'] += 1
return None
# 如果还有重试机会
if attempt < self.max_retries - 1:
wait_time = self._calculate_backoff(attempt)
logger.info(f"⏳ 等待 {wait_time:.2f} 秒后重试...")
time.sleep(wait_time)
else:
logger.error(f"💔 达到最大重试次数,放弃: {url}")
self.stats['failed_requests'] += 1
return None
return None
def _should_retry(self, exception: RequestException) -> bool:
"""判断错误是否应该重试"""
from requests.exceptions import (
ConnectionError, Timeout, HTTPError, TooManyRedirects
)
# 网络错误 → 重试
if isinstance(exception, (ConnectionError, Timeout)):
return True
# HTTP 错误
if isinstance(exception, HTTPError):
if hasattr(exception, 'response') and exception.response is not None:
status_code = exception.response.status_code
return status_code in self.RETRYABLE_CODES
# 重定向过多 → 不重试
if isinstance(exception, TooManyRedirects):
return False
# 默认重试
return True
def _calculate_backoff(self, attempt: int) -> float:
"""计算退避时间(指数退避 + 随机抖动)"""
delay = min(self.base_delay * (2 ** attempt), self.max_delay)
jitter = random.uniform(0, delay * 0.1)
return delay + jitter
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
total = self.stats['total_requests']
success = self.stats['successful_requests']
stats = self.stats.copy()
if total > 0:
stats['success_rate'] = f"{success/total*100:.2f}%"
if success > 0:
avg_time = self.stats['total_time'] / success
stats['avg_response_time'] = f"{avg_time:.3f}s"
return stats
def print_stats(self):
"""打印统计信息"""
stats = self.get_stats()
print("\n" + "=" * 60)
print("📊 请求统计")
print("=" * 60)
print(f"总请求数: {stats['total_requests']}")
print(f"成功: {stats['successful_requests']}")
print(f"失败: {stats['failed_requests']}")
print(f"总重试次数: {stats['total_retries']}")
print(f"成功率: {stats.get('success_rate', 'N/A')}")
print(f"平均响应时间: {stats.get('avg_response_time', 'N/A')}")
print("=" * 60 + "\n")
def close(self):
"""关闭 Session"""
self.session.close()
# ========== 使用示例 ==========
if __name__ == "__main__":
# 创建客户端
client = HttpClient(max_retries=3, base_delay=1.0)
# 测试URL列表
test_urls = [
"https://httpbin.org/get", # 正常请求
"https://httpbin.org/status/500", # 服务器错误(会重试)
"https://httpbin.org/status/404", # 404错误(不重试)
"https://httpbin.org/delay/3", # 延迟响应
"https://invalid-domain-12345.com", # 连接失败(会重试)
]
print("🚀 开始测试...")
for url in test_urls:
print(f"\n{'='*60}")
print(f"测试: {url}")
print(f"{'='*60}")
response = client.get(url)
if response:
print(f"✅ 状态码: {response.status_code}")
else:
print(f"❌ 请求失败")
time.sleep(1) # 测试间隔
# 打印统计
client.print_stats()
# 关闭客户端
client.close()
五、失败日志设计
5.1 日志级别规范
python
import logging
# 配置多级别日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.FileHandler('spider.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 使用示例
logger.debug("调试信息:请求参数详情")
logger.info("普通信息:开始抓取")
logger.warning("警告信息:重试第3次")
logger.error("错误信息:请求失败")
logger.critical("严重错误:程序崩溃")
5.2 结构化日志
python
import json
def log_request_failure(url, error, attempt, max_retries):
"""记录请求失败的详细信息"""
log_data = {
'timestamp': datetime.now().isoformat(),
'url': url,
'error_type': type(error).__name__,
'error_message': str(error),
'attempt': attempt,
'max_retries': max_retries,
'
}
logger.error(json.dumps(log_data, ensure_ascii=False))
六、本节小结
本节我们学习了爬虫稳定性的核心技能:
✅ 超时设置 :避免程序卡死,合理配置连接和读取超时
✅ 智能重试 :指数退避算法,逐步增加重试间隔
✅ 错误分类 :区分可重试和不可重试的错误
✅ 生产级客户端 :HttpClient 封装,统计监控功能
✅ 日志设计:结构化记录,便于问题排查
核心原则:
- 超时必设:所有请求都要设置超时
- 智能重试:不是所有错误都该重试
- 指数退避:避免瞬时压力过大
- 统计监控:记录成功率和响应时间
七、课后作业(必做,验收进入下一节)
任务1:测试 HttpClient
使用本节的 HttpClient 类,测试以下场景并截图:
python
client = HttpClient(max_retries=3, base_delay=1.0)
# 场景1:正常请求
client.get("https://httpbin.org/get")
# 场景2:会重试的错误
client.get("https://httpbin.org/status/503")
# 场景3:不会重试的错误
client.get("https://httpbin.org/status/404")
# 打印统计
client.print_stats()
任务2:扩展功能
为 HttpClient 添加以下方法:
python
def set_headers(self, headers):
"""批量设置请求头"""
pass
def enable_proxy(self, proxy_url):
"""启用代理"""
pass
def save_failed_urls(self, filename):
"""保存所有失败的URL到文件"""
pass
任务3:真实场景压测
选择一个稳定的网站,批量请求 20 个 URL:
- 记录总耗时
- 统计成功率
- 观察重试情况
- 分析日志中的错误类型
验收方式:在留言区提交:
- 测试运行的完整日志截图
- 扩展功能代码
- 压测统计报告
- 学习心得和问题
🔮下期预告
下一节《列表页→详情页:两段式采集(90%项目都这样)》,我们将学习:
- 交付:抓取 1 个列表页的 N 条详情链接并逐个采集
- 大纲:队列、去重集合、失败重试;"先小样本跑通再扩大"
💬 稳定性是爬虫的生命线!重试机制让你的程序更可靠!
记住:好的爬虫不是从不出错,而是出错后能优雅地恢复。工程师要为最坏的情况做好准备!
🌟文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。