第11章 爬虫监控与日志管理
11.1 添加日志记录:logging 模块配置
在爬虫开发过程中,日志记录是必不可少的环节。它不仅能帮助我们追踪程序运行状态,还能在出现问题时快速定位错误。Python 的 logging 模块提供了强大的日志功能,让我们可以轻松地记录不同级别的信息。
logging 模块基础配置
logging 模块支持多种日志级别:DEBUG、INFO、WARNING、ERROR 和 CRITICAL。对于爬虫项目,我们通常需要记录请求状态、解析结果和异常信息。
# 导入 logging 模块
import logging
# 配置日志格式和级别
logging.basicConfig(
level=logging.INFO, # 设置日志级别为 INFO
format='%(asctime)s - %(levelname)s - %(message)s', # 设置日志格式
handlers=[
logging.FileHandler('crawler.log', encoding='utf-8'), # 输出到文件
logging.StreamHandler() # 同时输出到控制台
]
)
# 记录不同级别的日志
logging.debug("这是一个调试信息") # 不会显示,因为级别低于 INFO
logging.info("爬虫开始运行") # 会显示
logging.warning("发现可疑响应") # 会显示
logging.error("请求失败") # 会显示实例方法表格
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 基础配置 | logging.basicConfig() | 配置日志的基本设置,必需参数包括 level、format,可选参数包括 handlers、filename 等 |
| 记录信息 | logging.info(message) | 记录一般信息,message 为必需的字符串参数 |
| 记录警告 | logging.warning(message) | 记录警告信息,表示可能有问题但不影响程序继续运行 |
| 记录错误 | logging.error(message) | 记录错误信息,通常用于捕获异常后的记录 |
使用示例
import logging
import requests
from urllib.parse import urljoin
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandlers('spider.log', encoding='utf-8'),
logging.StreamHandler()
]
)
# 创建专门的日志记录器
logger = logging.getLogger('MySpider')
def fetch_page(url):
"""获取网页内容并记录日志"""
try:
# 记录请求开始
logger.info(f"开始请求: {url}")
# 发送请求
response = requests.get(url, timeout=10)
response.raise_for_status() # 检查HTTP状态码
# 记录成功信息
logger.info(f"请求成功: {url}, 状态码: {response.status_code}")
return response.text
except requests.exceptions.RequestException as e:
# 记录错误信息
logger.error(f"请求失败: {url}, 错误: {str(e)}")
return None
# 测试函数
if __name__ == "__main__":
test_url = "https://httpbin.org/get"
content = fetch_page(test_url)
if content:
logger.info("页面获取成功,准备解析...")
else:
logger.warning("页面获取失败,跳过解析")注意事项:
- 日志文件路径要确保程序有写入权限
- 在生产环境中,建议将日志级别设置为 WARNING 或 ERROR,避免日志文件过大
- 对于多线程爬虫,需要考虑日志的线程安全性
- 日志格式中的时间戳默认使用本地时区,如需 UTC 时间可进行额外配置
日志记录是爬虫监控的基础,通过合理的日志配置,我们可以清晰地了解爬虫的运行状态和问题所在。
11.2 监控成功率与失败重试机制
在实际爬虫项目中,网络请求失败是常见现象。为了提高数据采集的成功率,我们需要建立完善的监控机制和重试策略。
请求成功率监控
成功率监控的核心是统计成功和失败的请求数量,并计算成功率百分比。这有助于我们评估爬虫的稳定性和目标网站的可访问性。
class RequestMonitor:
"""请求监控器类"""
def __init__(self):
self.success_count = 0 # 成功请求数
self.failure_count = 0 # 失败请求数
self.total_requests = 0 # 总请求数
def record_success(self):
"""记录成功请求"""
self.success_count += 1
self.total_requests += 1
def record_failure(self):
"""记录失败请求"""
self.failure_count += 1
self.total_requests += 1
def get_success_rate(self):
"""计算成功率"""
if self.total_requests == 0:
return 0.0
return (self.success_count / self.total_requests) * 100
def get_statistics(self):
"""获取统计信息"""
return {
'total': self.total_requests,
'success': self.success_count,
'failure': self.failure_count,
'success_rate': f"{self.get_success_rate():.2f}%"
}失败重试机制
重试机制需要考虑重试次数、重试间隔和退避策略。简单的线性重试可能会对服务器造成压力,而指数退避策略更为友好。
import time
import random
from functools import wraps
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
"""
重试装饰器
参数:
max_attempts: 最大重试次数
delay: 初始延迟时间(秒)
backoff: 退避因子(每次重试延迟时间乘以该因子)
exceptions: 需要重试的异常类型
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts - 1:
# 最后一次尝试失败,抛出异常
raise e
# 记录重试信息
logging.warning(f"第{attempt + 1}次尝试失败: {str(e)},{current_delay}秒后重试")
# 等待指定时间
time.sleep(current_delay)
# 应用退避策略
current_delay *= backoff
# 添加随机抖动,避免多个爬虫同时重试
current_delay += random.uniform(0, 1)
return None
return wrapper
return decorator实例方法表格
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 记录成功 | monitor.record_success() | 记录一次成功的请求,无参数 |
| 记录失败 | monitor.record_failure() | 记录一次失败的请求,无参数 |
| 获取统计 | monitor.get_statistics() | 返回包含统计信息的字典,无参数 |
| 重试装饰 | @retry(max_attempts=3, delay=1) | 装饰需要重试的函数,必需参数为被装饰的函数 |
使用示例
import logging
import requests
from urllib.parse import urljoin
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 创建监控器实例
monitor = RequestMonitor()
@retry(max_attempts=3, delay=2, backoff=1.5, exceptions=(requests.RequestException,))
def fetch_with_retry(url):
"""带重试机制的请求函数"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
monitor.record_success() # 记录成功
return response.text
except requests.RequestException as e:
monitor.record_failure() # 记录失败
raise e # 重新抛出异常,让重试机制处理
# 测试监控和重试
if __name__ == "__main__":
test_urls = [
"https://httpbin.org/get",
"https://httpbin.org/status/500", # 这个会返回500错误
"https://httpbin.org/delay/3" # 这个会有延迟
]
for url in test_urls:
try:
content = fetch_with_retry(url)
logger.info(f"成功获取: {url}")
except Exception as e:
logger.error(f"最终失败: {url}, 错误: {str(e)}")
# 输出统计信息
stats = monitor.get_statistics()
logger.info(f"爬取统计: {stats}")注意事项:
- 重试次数不宜过多,通常 2-3 次即可,避免浪费资源
- 退避策略可以有效减少对目标服务器的压力
- 对于明确的永久性错误(如404),不应该重试
- 监控数据应该定期保存,避免程序意外终止导致数据丢失
- 在分布式爬虫中,需要考虑监控数据的聚合问题
通过监控成功率和实现智能重试机制,我们可以显著提高爬虫的健壮性和数据采集效率。
11.3 使用 tqdm 显示进度条
在处理大量数据或长时间运行的爬虫任务时,进度条能够直观地展示任务完成情况,提升用户体验。Python 的 tqdm 库是实现进度条的最佳选择之一。
tqdm 基础用法
tqdm 可以轻松地包装任何可迭代对象,在循环过程中显示进度条。它会自动计算剩余时间、处理速度等信息。
from tqdm import tqdm
import time
# 基础进度条
for i in tqdm(range(100)):
time.sleep(0.01) # 模拟工作
# 自定义描述
for i in tqdm(range(100), desc="处理数据"):
time.sleep(0.01)在爬虫中集成进度条
在爬虫场景中,我们通常需要处理 URL 列表或分页数据。tqdm 可以很好地集成到这些场景中。
import requests
from tqdm import tqdm
def crawl_urls(url_list):
"""批量爬取URL列表"""
results = []
# 包装URL列表显示进度条
for url in tqdm(url_list, desc="爬取进度"):
try:
response = requests.get(url, timeout=10)
results.append(response.text)
except Exception as e:
results.append(None)
return results手动更新进度条
有时候我们的任务不是简单的循环,而是复杂的异步操作或条件分支。这时可以使用手动更新模式。
from tqdm import tqdm
# 创建进度条对象
pbar = tqdm(total=100, desc="自定义进度")
# 手动更新
for i in range(10):
# 模拟一些工作
time.sleep(0.1)
# 更新进度(可以指定增加的数量)
pbar.update(10)
# 关闭进度条
pbar.close()实例方法表格
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 自动进度 | tqdm(iterable) | 包装可迭代对象自动显示进度,iterable 为必需参数 |
| 自定义描述 | tqdm(iterable, desc="描述") | 设置进度条前缀描述,desc 为可选参数 |
| 手动创建 | tqdm(total=n) | 创建指定总量的进度条,total 为必需参数 |
| 更新进度 | pbar.update(n) | 手动增加进度,n 为可选参数,默认为1 |
使用示例
import requests
import logging
from tqdm import tqdm
from urllib.parse import urljoin
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def fetch_page(url):
"""获取单个页面"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.text
except Exception as e:
logger.error(f"获取页面失败: {url}, 错误: {str(e)}")
return None
def crawl_with_progress(base_url, page_count):
"""带进度条的分页爬虫"""
results = []
# 创建URL列表
urls = [f"{base_url}?page={i}" for i in range(1, page_count + 1)]
# 使用tqdm包装URL列表
for url in tqdm(urls, desc="分页爬取", unit="页"):
content = fetch_page(url)
results.append({
'url': url,
'content': content,
'success': content is not None
})
return results
# 实际使用示例(使用httpbin模拟)
if __name__ == "__main__":
# 注意:httpbin的/delay端点最大延迟为10秒
base_url = "https://httpbin.org/delay/1"
page_count = 5
logger.info("开始带进度条的爬虫任务")
results = crawl_with_progress(base_url, page_count)
# 统计结果
success_count = sum(1 for r in results if r['success'])
logger.info(f"爬取完成! 成功: {success_count}/{len(results)}")注意事项:
- tqdm 在 Jupyter Notebook 中有特殊版本
tqdm.notebook - 在多线程或多进程中使用 tqdm 需要特殊处理
- 进度条会占用终端的一行,如果程序还有其他输出,可能会造成显示混乱
- 对于非常快速的操作(毫秒级),进度条的刷新可能会影响性能
- 可以通过
disable=True参数在不需要时禁用进度条
进度条虽然看似简单,但能极大地改善长时间运行任务的用户体验,让用户清楚地知道任务的进展和预计完成时间。
11.4 异常分类处理:超时、连接错误、解析失败
在爬虫开发中,异常处理是保证程序稳定性的关键。不同类型的异常需要不同的处理策略,盲目地捕获所有异常可能会掩盖真正的问题。
常见异常类型分类
Python 爬虫中常见的异常主要分为以下几类:
- 网络连接异常:DNS 解析失败、连接超时、连接被拒绝等
- HTTP 协议异常:HTTP 状态码错误(4xx、5xx)
- 超时异常:请求超时、读取超时
- 解析异常:HTML 解析错误、JSON 解析错误
- 其他异常:内存不足、磁盘空间不足等系统级异常
requests 库异常体系
requests 库提供了完善的异常体系,我们可以针对性地处理不同类型的错误:
import requests
from requests.exceptions import (
ConnectionError, # 连接错误
Timeout, # 超时错误
HTTPError, # HTTP错误(状态码>=400)
RequestException # 所有requests异常的基类
)BeautifulSoup 解析异常
HTML 解析也可能出现异常,特别是当页面结构不符合预期时:
from bs4 import BeautifulSoup, FeatureNotFound
try:
soup = BeautifulSoup(html_content, 'html.parser')
except FeatureNotFound:
# 解析器不可用
soup = BeautifulSoup(html_content, 'html5lib')异常分类处理策略
针对不同类型的异常,我们应该采用不同的处理策略:
- 连接错误:通常是临时性问题,适合重试
- 超时错误:可以增加超时时间或重试
- HTTP 4xx 错误:客户端错误,通常不应该重试(429 除外)
- HTTP 5xx 错误:服务器错误,适合重试
- 解析错误:可能是页面结构变化,需要人工检查
实例方法表格
| 功能名称 | 实例调用方法 | 具体功能、注意事项、必需参数/可选参数 |
|---|---|---|
| 连接错误处理 | except ConnectionError as e | 处理网络连接相关错误,如DNS失败、连接拒绝 |
| 超时处理 | except Timeout as e | 处理请求超时或读取超时,可调整timeout参数 |
| HTTP错误处理 | except HTTPError as e | 处理HTTP状态码错误,可通过response.status_code获取具体状态码 |
| 通用异常处理 | except RequestException as e | 捕获所有requests相关的异常 |
使用示例
import requests
import logging
from bs4 import BeautifulSoup
from requests.exceptions import ConnectionError, Timeout, HTTPError, RequestException
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def safe_fetch_and_parse(url, timeout=10):
"""
安全地获取并解析网页
参数:
url: 目标URL
timeout: 超时时间(秒)
返回:
dict: 包含成功状态、内容和错误信息的字典
"""
result = {
'success': False,
'content': None,
'error_type': None,
'error_message': None
}
try:
# 发送请求
logger.info(f"请求URL: {url}")
response = requests.get(url, timeout=timeout)
response.raise_for_status() # 检查HTTP状态码
# 解析HTML
soup = BeautifulSoup(response.text, 'html.parser')
# 提取标题作为示例
title = soup.find('title')
result['content'] = title.get_text() if title else "No title found"
result['success'] = True
logger.info(f"成功解析: {url}")
except ConnectionError as e:
# 连接错误:网络问题、DNS解析失败等
error_msg = f"连接错误: {str(e)}"
logger.error(error_msg)
result['error_type'] = 'connection_error'
result['error_message'] = error_msg
except Timeout as e:
# 超时错误:请求或读取超时
error_msg = f"超时错误: {str(e)}"
logger.error(error_msg)
result['error_type'] = 'timeout_error'
result['error_message'] = error_msg
except HTTPError as e:
# HTTP错误:4xx或5xx状态码
status_code = e.response.status_code if e.response else 'Unknown'
error_msg = f"HTTP错误 (状态码: {status_code}): {str(e)}"
logger.error(error_msg)
# 根据状态码决定是否重试
if status_code == 429: # Too Many Requests
result['error_type'] = 'rate_limit'
elif 500 <= status_code < 600: # 服务器错误
result['error_type'] = 'server_error'
else: # 客户端错误
result['error_type'] = 'client_error'
result['error_message'] = error_msg
except Exception as e:
# 其他未预期的错误
error_msg = f"未知错误: {str(e)}"
logger.error(error_msg)
result['error_type'] = 'unknown_error'
result['error_message'] = error_msg
return result
# 测试不同类型的异常
if __name__ == "__main__":
test_cases = [
"https://httpbin.org/get", # 正常情况
"https://nonexistent-domain-12345.com", # 连接错误
"https://httpbin.org/delay/15", # 超时错误(如果timeout<15)
"https://httpbin.org/status/404", # HTTP 404错误
"https://httpbin.org/status/500", # HTTP 500错误
]
for url in test_cases:
result = safe_fetch_and_parse(url, timeout=5)
print(f"\nURL: {url}")
print(f"成功: {result['success']}")
if not result['success']:
print(f"错误类型: {result['error_type']}")
print(f"错误信息: {result['error_message'][:50]}...") # 截断长信息注意事项:
- 异常处理应该具体而不是笼统,避免使用裸露的
except: - 对于可恢复的错误(如网络超时、服务器错误),应该实现重试机制
- 对于不可恢复的错误(如404、403),应该记录并跳过,而不是无限重试
- 异常信息应该包含足够的上下文,便于后续分析和调试
- 在生产环境中,应该将异常分类统计数据定期汇总,用于优化爬虫策略
通过精细化的异常分类处理,我们可以让爬虫更加智能和健壮,既能应对各种异常情况,又能为后续的优化提供有价值的数据。