一、 问题根源:为什么爬虫会遭遇403?
在构建解决方案之前,我们首先需要理解敌人。服务器返回403通常基于以下几点:
- User-Agent识别:服务器检测到请求来自非浏览器客户端(如Python-Requests、Scrapy),遂拒绝服务。
- IP频率限制:单个IP在单位时间内的请求频率过高,触发服务器的防爬虫策略。
- 缺少Referer头:对于某些通过链接跳转访问的资源,服务器会验证
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Referer</font>头。 - Cookie或Session验证:需要特定登录状态或令牌的页面,匿名访问会被拒绝。
- 高级指纹检测:如TLS指纹、浏览器API支持等,这在Scrapy中相对少见,但在Selenium等驱动浏览器中更常见。
二、 解决方案:Scrapy下载器中间件
Scrapy的架构之美在于其高度的可扩展性。下载器中间件是位于Scrapy引擎和下载器之间的钩子框架,用于全局处理请求和响应。这正是我们统一处理403状态的理想场所。
我们的核心思路是:创建一个自定义中间件,捕获所有状态码为403的响应,并按照预设策略自动重试该请求,同时在重试前对请求进行“修饰”以绕过检测。
实现步骤与代码
我们将创建一个名为 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Http403RetryMiddleware</font> 的中间件。
步骤1:创建项目与中间件文件
假设你已经有一个Scrapy项目。如果没有,可以通过 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">scrapy startproject myproject</font> 创建。我们在项目的 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">middlewares.py</font> 文件中定义我们的中间件。
python
# middlewares.py
import random
import logging
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
logger = logging.getLogger(__name__)
class Http403RetryMiddleware(RetryMiddleware):
"""
自定义403重试中间件,继承自Scrapy内置的RetryMiddleware。
这样我们可以复用其重试逻辑和设置(如重试次数、重试延迟)。
"""
def process_response(self, request, response, spider):
# 核心方法:处理响应
# 如果响应状态码不是403,交给父类处理(处理429、500等)
if response.status != 403:
return super().process_response(request, response, spider)
# 记录警告日志
logger.warning(f"Intercepted 403 Forbidden for: {request.url}")
# 调用重试方法
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
def process_request(self, request, spider):
"""
在请求发送前,对其进行“修饰”,增加反反爬虫措施。
这里会在每次重试(包括第一次)时被调用。
"""
self._enhance_request(request)
# 返回None,Scrapy会继续处理这个请求
return None
def _enhance_request(self, request):
"""
增强请求,添加或修改请求头,模拟浏览器行为。
"""
# 1. 设置一个常见的、真实的User-Agent池
user_agent_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
]
# 如果请求没有设置User-Agent,或者我们想要在重试时更换,可以在这里设置
if not request.headers.get('User-Agent'):
request.headers['User-Agent'] = random.choice(user_agent_list)
# 2. 设置Referer头,可以设置为同域名下的一个安全页面,或者谷歌
if not request.headers.get('Referer'):
# 这里简单设置为同域名的根目录,实际项目中可以根据情况动态设置
domain = request.url.split('/')[2] # 获取域名
request.headers['Referer'] = f'https://{domain}/'
# 3. 设置其他常见的请求头,使其更像浏览器
request.headers.setdefault('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8')
request.headers.setdefault('Accept-Language', 'zh-CN,zh;q=0.9,en;q=0.8')
request.headers.setdefault('Accept-Encoding', 'gzip, deflate, br')
request.headers.setdefault('DNT', '1')
request.headers.setdefault('Connection', 'keep-alive')
request.headers.setdefault('Upgrade-Insecure-Requests', '1')
logger.debug(f"Enhanced request headers for: {request.url}")
步骤2:启用中间件并配置
仅仅创建中间件是不够的,我们需要在Scrapy项目的设置文件中启用它,并调整相关配置。
打开 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">settings.py</font> 文件,进行如下修改:
python
# settings.py
# 下载器中间件配置
DOWNLOADER_MIDDLEWARES = {
# 首先停用内置的RetryMiddleware,因为我们自定义的中间件继承了它并会替代其功能。
'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
# 然后添加我们自定义的403重试中间件,优先级数字可以自定义(500-600是常用范围)
'myproject.middlewares.Http403RetryMiddleware': 550,
# 其他中间件...
}
# 重试设置
# 总的重试次数(包括第一次请求)
RETRY_TIMES = 3
# 需要重试的HTTP状态码,确保403在其中
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429, 403] # 重点:加入了403
# 随机下载延迟,避免请求过于规律
DOWNLOAD_DELAY = 1
AUTOTHROTTLE_ENABLED = True # 推荐启用自动限速
# 并发请求数,根据目标网站承受能力调整
CONCURRENT_REQUESTS = 16
# 为特定网站设置自定义配置(可选)
CUSTOM_USER_AGENT = 'Mozilla/5.0 (compatible; MyBot/1.0; +http://mycompany.com)'
步骤3:在Spider中应用(可选高级技巧)
你还可以在Spider内部根据特定站点的需求,微调中间件的行为。例如,为某个特定的难啃的网站准备一个独立的User-Agent列表。
python
# spiders/__init__.py
# 假设我们在spider中设置了自定义的meta来指导中间件
class MySpider(scrapy.Spider):
name = 'myspider'
def start_requests(self):
urls = ['https://example.com/page1', 'https://example-harder-site.com/page2']
for url in urls:
# 对于难处理的网站,可以设置一个标志,让中间件使用更激进的策略
meta = {}
if 'harder-site' in url:
meta['use_aggressive_retry'] = True
yield scrapy.Request(url=url, callback=self.parse, meta=meta)
def parse(self, response):
# ... 你的解析逻辑
pass
然后,在中间件中,我们可以读取这个 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">meta</font> 信息:
python
# 在 middlewares.py 的 process_request 方法中添加
def process_request(self, request, spider):
# 检查spider的meta中是否有特殊指令
if request.meta.get('use_aggressive_retry'):
# 例如,更换一个不同的、更复杂的User-Agent池
aggressive_agents = [...]
request.headers['User-Agent'] = random.choice(aggressive_agents)
else:
self._enhance_request(request) # 使用默认的增强方法
return None
三、 工程化优势与总结
通过上述实现,我们成功地将403错误处理工程化:
- 统一处理:项目中所有Spider发出的请求,一旦遇到403,都会自动触发重试机制,无需在每个Spider中重复编写错误处理代码。
- 策略集中:所有反反爬虫的“修饰”逻辑(如更换User-Agent、添加Headers)都集中在中间件中,便于维护和更新。例如,当某个User-Agent失效时,只需在一个地方更新列表。
- 充分利用框架:继承自
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">RetryMiddleware</font>,使我们能无缝接入Scrapy的重试、记录日志和统计系统。 - 灵活可扩展:该中间件可以轻松扩展以应对更复杂的情况,例如:
- 集成代理IP:在
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">process_request</font>中,当重试次数超过一定阈值时,为请求设置代理<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">request.meta['proxy'] = some_proxy_url</font>。 - 模拟登录:如果403是由于未登录引起的,可以在中间件中检查并触发一个登录流程。
- 动态指纹应对:与更复杂的库(如
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">curl_cffi</font>或<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">selenium</font>)结合,在特定条件下使用它们来执行请求。
- 集成代理IP:在
结论:
在爬虫开发中,处理反爬虫机制不应是事后补救的散兵游勇,而应是项目初期就纳入设计的系统工程。利用Scrapy中间件对403状态码进行统一、智能化的处理,是迈向稳健、可维护、高效率的数据采集系统的关键一步。本文提供的方案不仅解决了403问题,更展示了一种工程化的思维模式,可举一反三应用于处理其他如429(请求过多)、JS挑战等复杂的爬虫挑战。