2026 年,Scrapy 依然是生产级爬取的主力——也依然是最让人在代理配置上犯迷糊的框架,因为有四个不同的地方可以插入代理,而其中三个对大多数项目来说都是错的。这篇指南给你那个对的:一个小巧的自定义 middleware,带逐请求路由、sticky 会话、封禁检测,以及理智的重试行为。
如果你用的是纯 requests/httpx/aiohttp,请看 如何在 Python 里轮换代理。本文专讲 Scrapy。
对于一个轮换住宅网关,最小可用配置就是每个请求一行——不需要 middleware:
def start_requests(self):
for url in self.urls:
yield scrapy.Request(
url,
meta={"proxy": "http://USERNAME:[email protected]:913"},
)
Scrapy 内置的 HttpProxyMiddleware 会读取 request.meta["proxy"],并从 URL 里处理认证。网关会替你轮换出口 IP。如果你只需要这些,到此为止即可。这份指南剩下的部分,是为你需要控制力时准备的:sticky 会话、国家路由、感知封禁的轮换,以及并发调优。
把这个放进 middlewares.py。它按域名分配 sticky 会话,遇到封禁就轮换,并给每个请求打标签,方便你调试是哪个会话抓了什么:
import random
import string
GATEWAY = "us.jibaoproxy.com:913"
USERNAME = "USERNAME" # 真实项目里请移到 settings.py / 环境变量
PASSWORD = "PASSWORD"
def _new_session(n=8):
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
class JibaoProxyMiddleware:
"""每个域名一个 sticky 会话;被封时轮换会话。"""
def __init__(self):
self.sessions = {} # 域名 -> 会话 id
def _proxy_url(self, session_id):
user = f"{USERNAME}-session-{session_id}"
return f"http://{user}:{PASSWORD}@{GATEWAY}"
def process_request(self, request, spider):
domain = request.url.split("/")[2]
session = self.sessions.setdefault(domain, _new_session())
request.meta["proxy"] = self._proxy_url(session)
request.meta["proxy_session"] = session
def rotate(self, domain):
"""会话被烧掉时调用。"""
self.sessions[domain] = _new_session()
再配一个下载器 middleware,它检测封禁并在新会话上重试:
from scrapy.downloadermiddlewares.retry import RetryMiddleware
from scrapy.utils.response import response_status_message
BAN_CODES = {403, 429}
BAN_MARKERS = (b"captcha", b"access denied", b"unusual traffic")
class BanAwareRetryMiddleware(RetryMiddleware):
def process_response(self, request, response, spider):
banned = (
response.status in BAN_CODES
or any(m in response.body[:2048].lower() for m in BAN_MARKERS)
)
if banned:
domain = request.url.split("/")[2]
proxy_mw = spider.crawler.engine.downloader.middleware.middlewares
for mw in proxy_mw:
if hasattr(mw, "rotate"):
mw.rotate(domain) # 烧掉这个会话
reason = response_status_message(response.status)
return self._retry(request, reason, spider) or response
return super().process_response(request, response, spider)
在 settings.py 里把两个都接上:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.JibaoProxyMiddleware": 350,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": None, # 替换掉自带的重试
"myproject.middlewares.BanAwareRetryMiddleware": 550,
}
RETRY_TIMES = 2
优先级很重要:代理 middleware 必须跑在 Scrapy 的 HttpProxyMiddleware(750)之前,所以任何低于 750 的值都行;350 让它足够早、也足够可预测。
| 爬取类型 | 模式 | 实现方式 |
|---|---|---|
| 无状态的页面采集 | 轮换 | 裸用户名,网关逐请求轮换 |
| 登录后在鉴权背后爬取 | 每账号 sticky | -session-{account_id},登录过程中绝不轮换 |
| 分页密集的列表页 | 每域名 sticky,被封时轮换 | 上面那个 middleware |
| 按地区的定价 | 轮换 + 国家锁定 | USERNAME-country-de 这类参数 |
对这一权衡更深入的探讨:Sticky 会话 vs 轮换代理会话。
Scrapy 的默认值是为礼貌的单 IP 爬取调的。在一个轮换池背后,你可以推得猛得多——但每域名的限制依然重要,因为目标看到的是聚合行为:
# settings.py - 住宅池背后理智的起点
CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 目标实际感受到的
DOWNLOAD_DELAY = 0.25 # 每个槽位施加的抖动
RANDOMIZE_DOWNLOAD_DELAY = True # 延迟的 0.5x-1.5x
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 6.0
DOWNLOAD_TIMEOUT = 30
ROBOTSTXT_OBEY = True
只有在当前档位下盯了几千个请求的 403 率之后,才去调高 CONCURRENT_REQUESTS_PER_DOMAIN。因为"反正代理会轮换"就把它从 8 → 32,正是人们在重试上烧光 GB 的典型方式。
IMAGES_STORE 管道设置成走数据中心代理或直连——媒体占了你绝大部分 GB,却很少受保护。HTTPCACHE_ENABLED = True,它会从磁盘回放响应,而不是重新通过代理池抓取。光这一个设置通常就能把一个项目的带宽账单砍掉一半。COMPRESSION_ENABLED = True(默认值)——gzip 过的 HTML 在传输时小 5–10 倍。407 Proxy Authentication Required凭证没到达代理。把它们放进 meta["proxy"] 的 URL 里(http://user:pass@host:port)——Scrapy 会替你解析并设置 Proxy-Authorization。手动设置请求头同时又在 URL 里带凭证,会引发双重认证的怪问题;二选一。
TunnelError: Could not open CONNECT tunnel几乎总是 host/port 打错了,或者 HTTPS 目标走了一个不允许在该端口上 CONNECT 的端点。先在 Scrapy 之外用 curl -x 验证一下。
你的 sticky 会话活过了它该有的寿命,或者你的每域名速率太猛。上面那个感知封禁的 middleware 能处理第一种情况;第二种情况就调低 CONCURRENT_REQUESTS_PER_DOMAIN。如果目标是个检查 JA4 的站点,那可能是 Scrapy 的 TLS 栈本身露了馅——见 JA3/JA4 详解,了解为什么没有任何代理能修这个。
meta["proxy"] 配上网关 URL 就是全部配置。新用户注册即送500M免费流量,首次充值额外加赠,活动期间限时开放。