Scrapy 在 2026 年仍是生產級爬取的主力——也仍是最讓人搞不懂代理設定的框架,因為有四個不同的地方可以插入代理,而其中三個對大多數專案來說都是錯的。本指南給你正確的那一個:一個帶有每請求路由、黏性工作階段、封鎖偵測與合理重試行為的小型自訂中介軟體。
如果你用的是純 requests/httpx/aiohttp,請參見 如何在 Python 中輪換代理。本文是 Scrapy 專屬的。
對於輪換式住宅閘道,最小可行設定就是每個請求一行——不需要中介軟體:
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。如果你需要的就這些,到此為止。本指南其餘部分是給你需要控制的時候:黏性工作階段、國家路由、封鎖感知的輪換,以及並行度調校。
把這段放進 middlewares.py。它替每個網域指派黏性工作階段、在被封鎖時輪換,並為每個請求加標籤,讓你能除錯是哪個工作階段抓了什麼:
import random
import string
GATEWAY = "us.jibaoproxy.com:913"
USERNAME = "USERNAME" # move to settings.py / env in real projects
PASSWORD = "PASSWORD"
def _new_session(n=8):
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
class JibaoProxyMiddleware:
"""Sticky session per domain; rotate session on ban."""
def __init__(self):
self.sessions = {} # domain -> session 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):
"""Call when a session is burned."""
self.sessions[domain] = _new_session()
再配一個下載器中介軟體,偵測封鎖並在新工作階段上重試:
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) # burn the session
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, # replace stock retry
"myproject.middlewares.BanAwareRetryMiddleware": 550,
}
RETRY_TIMES = 2
優先順序很重要:代理中介軟體必須在 Scrapy 的 HttpProxyMiddleware(750)之前執行,所以任何低於 750 的值都行;350 讓它保持在前面且可預期。
| 爬取類型 | 模式 | 實作 |
|---|---|---|
| 無狀態頁面採集 | 輪換 | 裸使用者名稱,閘道每請求輪換 |
| 登入 + 在驗證後爬取 | 每帳號黏性 | -session-{account_id},登入途中絕不輪換 |
| 分頁繁重的列表 | 每網域黏性,被封鎖時輪換 | 上面的中介軟體 |
| 特定地區定價 | 輪換 + 國家固定 | USERNAME-country-de 式的參數 |
對此取捨的更深入探討:黏性 vs 輪換代理工作階段。
Scrapy 的預設值是為禮貌的單 IP 爬取調的。在輪換池後面你可以推得更猛——但每網域限制仍然重要,因為目標看到的是聚合行為:
# settings.py - sane starting point behind a residential pool
CONCURRENT_REQUESTS = 64
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # what the target experiences
DOWNLOAD_DELAY = 0.25 # jitter applied per slot
RANDOMIZE_DOWNLOAD_DELAY = True # 0.5x-1.5x the delay
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幾乎都是主機/連接埠打錯字,或 HTTPS 目標走了一個不允許在該連接埠用 CONNECT 的端點。先在 Scrapy 之外用 curl -x 驗證。
你的黏性工作階段活過頭了,或你的每網域速率太燙。上面的封鎖感知中介軟體處理第一種情況;第二種就調低 CONCURRENT_REQUESTS_PER_DOMAIN。如果是個檢查 JA4 的目標,Scrapy 的 TLS 堆疊本身可能就是破綻——參見 JA3/JA4 詳解 看為何沒有代理能修好那個。
meta["proxy"] 就是全部的設定。新用戶註冊即送 500M免費流量,首次儲值另有贈金。優惠限時供應。