为什么Scrapy集成动态代理IP不能只写一行proxy?

只写一行 proxy 会在并发上线后立刻塌方,因为动态代理 IP 有生存周期、有失败率、有并发上限,任何一项没处理都会传导到爬取失败。

一个只塞 proxy 字段的最小实现,遇到的第一个坑是 IP 时效。短效动态 IP 存活周期常见在 1 到 5 分钟,请求发出前刚从池里取的 IP,网络往返回来时可能已经失效。第二个坑是失败传导,一个 IP 挂了如果没有识别与下架逻辑,同一批任务会在死 IP 上反复重试直到全部超时。第三个坑是会话粘性缺失,登录态、加购、翻页这类需要同一出口 IP 的业务,随机换 IP 直接把状态打乱。

在网站采集器、舆情监测、拓客数据这三类典型场景里,这三个坑是并存的。舆情监测的高并发放大 IP 池的失效率,拓客数据的登录态放大会话粘性问题,采集器的多目标又要求失败重试逻辑要能识别不同状态码。

简单塞proxy的做法会踩到的坑生产环境后果
从txt文件读IP列表轮换IP过期没剔除大量ConnectionRefused
请求前随机挑一个IP不识别失败状态码重试打在同一个死IP上
全局固定一个proxy没有Session-IP绑定登录态、Cookie被打乱
每请求都换IP无并发调度池被打空、请求排队

Scrapy应该选API提取还是隧道代理

看并发规模和是否需要精细控制 IP 生命周期:API 提取适合大规模、需要在客户端做池管理的场景;隧道适合中小规模、快速接入、不想自己维护池。

两者的差异不在协议,而在"谁来管 IP 池"。API 提取模式下,客户端定时调用 API 拉一批 IP,池的大小、TTL、换 IP 时机全由客户端决定。隧道模式下,客户端只对一个固定的隧道地址发请求,代理服务方在后端每隔 N 秒或每个请求换一个出口 IP。

维度API提取模式隧道模式
接入方式定时调用API拉取IP,本地维护池请求打到固定隧道地址
换IP时机客户端主动切换服务方后端自动切换
客户端复杂度需自建IP池、TTL管理、失败下架中间件只需一次配置
会话粘性显式绑定session_id到IP通过sticky参数或user名带session
并发承压池大小需按QPS预估由服务方承压
失败重试需自己下架IP再取新的直接重发即可
典型适配场景舆情监测高并发采集、网站采集器批量任务拓客数据中小规模采集、快速验证选型
中间件代码量多,含池管理模块少,仅设置proxy和鉴权头

一个经验值参考:单机 Scrapy CONCURRENT_REQUESTS 在 32 以下、单业务、无强会话粘性诉求,优先隧道模式,接入成本最低;CONCURRENT_REQUESTS 超过 64、或者多业务共用、或者需要 Session-IP 绑定,走 API 提取模式的收益开始超过它的复杂度。

API提取模式下的Scrapy代理中间件怎么写?

先写一个线程安全的 IP 池组件,再把它接进 Downloader Middleware 的三个钩子process_request 挂代理,process_response 识别失败下架,process_exception 处理连接异常。

第一步:IP池组件

# proxy_pool.py
import time
import threading
import requests
from collections import deque

class ProxyPool:
    def __init__(self, api_url, refresh_size=20, ttl=180, buffer_seconds=15):
        self.api_url = api_url
        self.refresh_size = refresh_size
        self.ttl = ttl
        self.buffer = buffer_seconds
        self.pool = deque()
        self.lock = threading.Lock()

    def _fetch(self):
        try:
            resp = requests.get(
                f"{self.api_url}?num={self.refresh_size}",
                timeout=5,
            )
            data = resp.json()
        except Exception:
            return
        now = time.time()
        for item in data.get("data", []):
            self.pool.append({
                "proxy": f"http://{item['ip']}:{item['port']}",
                "expire_at": now + self.ttl,
            })

    def get(self):
        with self.lock:
            now = time.time()
            while self.pool:
                item = self.pool[0]
                if item["expire_at"] <= now + self.buffer:
                    self.pool.popleft()
                    continue
                return item["proxy"]
            self._fetch()
            if self.pool:
                return self.pool[0]["proxy"]
        return None

    def report_dead(self, proxy):
        with self.lock:
            self.pool = deque(x for x in self.pool if x["proxy"] != proxy)

关键点有三个。TTL 里减掉 buffer_seconds,避免取到"刚好在往返途中过期"的 IP。report_dead 用整表过滤保证同一失效 IP 不会被再次取出。池空时同步拉新,避免请求阻塞在等待补池上。

第二步:代理中间件

# middlewares.py
from .proxy_pool import ProxyPool

class DynamicProxyMiddleware:
    def __init__(self, api_url):
        self.pool = ProxyPool(api_url)

    @classmethod
    def from_crawler(cls, crawler):
        api_url = crawler.settings.get("PROXY_API_URL")
        return cls(api_url)

    def process_request(self, request, spider):
        if request.meta.get("proxy"):
            return
        proxy = self.pool.get()
        if proxy:
            request.meta["proxy"] = proxy
            request.meta["_proxy_used"] = proxy

    def process_response(self, request, response, spider):
        return response

    def process_exception(self, request, exception, spider):
        proxy = request.meta.get("_proxy_used")
        if proxy:
            self.pool.report_dead(proxy)
        return None

process_request 里加 if request.meta.get("proxy") 判断,是为了兼容上层 SessionProxyMiddleware 已经绑定 IP 的场景。_proxy_used 单独存一份,防止重试时 proxy 字段被别的中间件改写、找不到原来那个死 IP 无法下架。

第三步:settings.py 挂载

# settings.py
DOWNLOADER_MIDDLEWARES = {
    'myspider.middlewares.SmartRetryMiddleware': 550,
    'myspider.middlewares.DynamicProxyMiddleware': 750,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None,
}
PROXY_API_URL = "https://proxy-api.example.com/get"
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 16
DOWNLOAD_TIMEOUT = 15
RETRY_ENABLED = False

内置 HttpProxyMiddleware 关掉,避免它把环境变量里的 http_proxy 覆盖进来。内置 RetryMiddleware 关掉,改用后面自定义的 SmartRetryMiddleware,因为默认重试不会下架失效 IP。

隧道模式下的Scrapy代理中间件怎么写?

代码量比 API 提取模式少一半,核心是设置 proxy 地址和一个 Basic Auth 头,无需自建池。

# middlewares.py
import base64

class TunnelProxyMiddleware:
    def __init__(self, tunnel, username, password):
        self.proxy = f"http://{tunnel}"
        auth = f"{username}:{password}".encode()
        self.auth_header = b"Basic " + base64.b64encode(auth)

    @classmethod
    def from_crawler(cls, crawler):
        s = crawler.settings
        return cls(
            tunnel=s.get("PROXY_TUNNEL"),
            username=s.get("PROXY_USERNAME"),
            password=s.get("PROXY_PASSWORD"),
        )

    def process_request(self, request, spider):
        request.meta["proxy"] = self.proxy
        request.headers[b"Proxy-Authorization"] = self.auth_header

    def process_exception(self, request, exception, spider):
        return None

隧道模式下,process_exception 不用做 IP 下架,直接返回 None 让 Scrapy 走到下一个中间件即可,服务方会自动换出口。

# settings.py 隧道模式片段
PROXY_TUNNEL = "tunnel.example.com:12345"
PROXY_USERNAME = "your_user"
PROXY_PASSWORD = "your_pass"

隧道模式有两个易踩的点。一是 Proxy-Authorization 头必须是 bytes 类型,用 str 会被 Scrapy 的 Header 序列化模块拒绝或去重。二是隧道服务方通常按连接维度换 IP,如果 Scrapy 复用 TCP 连接会导致多个请求走同一个出口 IP,需要在 settings 里配 DOWNLOAD_HANDLERS 关闭连接复用或在请求层拆分,具体做法看服务方文档。

遇到403/407/超时,Scrapy代理中间件该怎么重试?

按状态码分类处理:代理层错误(407/连接异常)立即下架 IP 并重试;目标站限制(403/429)下架 IP 加短延时重试;服务器 5xx 换 IP 重试;重试次数封顶避免死循环。

状态码/异常含义建议动作是否下架IP
200/301/302正常放行
403目标站访问频率控制换IP重试,延时0.5-2秒
407代理鉴权失败检查账密,重试1次
429目标站限速换IP重试+延时5-10秒
502/503/504代理网关异常换IP直接重试
ConnectionError代理不可达换IP直接重试
TimeoutError连接超时换IP直接重试
200但内容为验证码页目标站识别换IP+清Cookie重试
# middlewares.py
class SmartRetryMiddleware:
    RETRIABLE_STATUS = {403, 429, 500, 502, 503, 504}

    def __init__(self, max_retry=3):
        self.max_retry = max_retry

    @classmethod
    def from_crawler(cls, crawler):
        return cls(max_retry=crawler.settings.getint("PROXY_MAX_RETRY", 3))

    def process_response(self, request, response, spider):
        if response.status not in self.RETRIABLE_STATUS:
            return response
        retry_times = request.meta.get("proxy_retry_times", 0)
        if retry_times >= self.max_retry:
            spider.logger.warning(
                f"Proxy retry exceeded: {request.url}, status={response.status}"
            )
            return response
        return self._build_retry(request, retry_times, reason=f"status_{response.status}")

    def process_exception(self, request, exception, spider):
        retry_times = request.meta.get("proxy_retry_times", 0)
        if retry_times >= self.max_retry:
            return None
        return self._build_retry(request, retry_times, reason=type(exception).__name__)

    def _build_retry(self, request, retry_times, reason):
        new_request = request.copy()
        new_request.meta["proxy_retry_times"] = retry_times + 1
        new_request.meta["proxy_retry_reason"] = reason
        new_request.meta.pop("proxy", None)
        new_request.meta.pop("_proxy_used", None)
        new_request.dont_filter = True
        return new_request

_build_retry 里显式 pop("proxy")pop("_proxy_used"),让下一次 DynamicProxyMiddleware.process_request 重新取一个新的 IP,而不是继续用死掉的那个。dont_filter=True 避免重试请求被 Scrapy 的去重指纹拦下。

在拓客数据这类需要区分"我方策略问题"和"目标站限制"的场景,proxy_retry_reason 便于后续做日志分析、判断到底是 IP 池质量差还是采集策略被限制。

需要IP会话粘性时,中间件怎么绑定Session和IP?

API 提取模式用一张 session_id 到 proxy 的映射表,隧道模式在账密里带 session 标识,两者都要处理"绑定的 IP 过期后如何续期"。

API提取模式:显式绑定表

# middlewares.py
import threading
from .proxy_pool import ProxyPool

class SessionProxyMiddleware:
    def __init__(self, api_url, session_ttl=900):
        self.pool = ProxyPool(api_url, ttl=session_ttl, refresh_size=10)
        self.session_map = {}
        self.lock = threading.Lock()
        self.session_ttl = session_ttl

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            api_url=crawler.settings.get("PROXY_STICKY_API_URL"),
            session_ttl=crawler.settings.getint("PROXY_SESSION_TTL", 900),
        )

    def process_request(self, request, spider):
        session_id = request.meta.get("session_id")
        if not session_id:
            return
        with self.lock:
            entry = self.session_map.get(session_id)
            if entry and entry["expire_at"] > time.time() + 30:
                request.meta["proxy"] = entry["proxy"]
                request.meta["_proxy_used"] = entry["proxy"]
                return
            proxy = self.pool.get()
            if proxy:
                self.session_map[session_id] = {
                    "proxy": proxy,
                    "expire_at": time.time() + self.session_ttl,
                }
                request.meta["proxy"] = proxy
                request.meta["_proxy_used"] = proxy

Spider 里生成会话时给一个 session_id

def start_requests(self):
    for i in range(100):
        yield scrapy.Request(
            url="https://example.com/login",
            meta={"session_id": f"session_{i}"},
            callback=self.after_login,
        )

session_ttl 建议对齐动态 IP 的最大生存周期,或者选长效代理 IP 池的 TTL(常见 15 到 30 分钟)。session_ttl 短于 IP 实际存活会浪费池资源,长于 IP 存活会导致会话中途换 IP。

隧道模式:账密带session

多数隧道服务方约定,在用户名里带 session- 前缀就可以让同一个 session 值走固定出口 IP,通常几分钟到几十分钟不等。

def process_request(self, request, spider):
    session_id = request.meta.get("session_id", "default")
    user = f"{self.username}-session-{session_id}"
    auth = f"{user}:{self.password}".encode()
    request.meta["proxy"] = self.proxy
    request.headers[b"Proxy-Authorization"] = (
        b"Basic " + base64.b64encode(auth)
    )

具体的 session 参数格式各家不同,接入前需查所选服务方的账密规则文档。

Scrapy代理中间件的常见性能问题有哪些?

四个高频问题:IP池空、鉴权失败、请求排队、锁竞争,都能通过参数调整或代码微调解决,不需要重写架构。

问题现象根本原因定位手段解决方案
日志频繁出现"proxy is None"refresh_size小或TTL短或QPS高打印 pool.size() 走势refresh_size ≥ 并发数; TTL对齐IP存活
大量407 Proxy Authentication Required账密错、Basic Auth编码错、字段名拼错单独用requests验证一次隧道检查Basic Auth的bytes类型和base64编码
请求排队严重、QPS上不去CONCURRENT_REQUESTS与池大小不匹配观察Scrapy的scheduler待处理数池大小 ≥ 2倍CONCURRENT_REQUESTS
内存持续上涨失败请求无上限重入打印retry_times分布PROXY_MAX_RETRY设为3-5
CPU占用高但IO低池管理lock竞争py-spy采样看lock等待拆分lock粒度或改用asyncio
目标站返回验证码页而非403未识别验证码状态抽样response.text在SmartRetryMiddleware加内容检测
短时突发失败率飙升池里的IP集中过期记录IP过期时间分布分批次拉新,错峰过期

在舆情监测这种长时间运行的场景,"分批次拉新"这一条特别关键。如果初始化时一次性拉满一个池、又不做错峰,会导致所有 IP 集中在同一分钟过期,出现周期性失败率脉冲。可以在 ProxyPool._fetch 里给每个 IP 的 expire_at 加一个 0 到 30 秒的随机抖动。

FAQ

Q:Scrapy自带的HttpProxyMiddleware和自定义代理中间件冲突怎么办?

在 settings.py 里把 scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware 设为 None 关掉即可。自带的中间件默认从环境变量 http_proxyhttps_proxy 里读代理,如果本地或容器里刚好设置过这些变量,会在自定义中间件挂 proxy 之前先把请求打到环境变量里的地址,导致排查时怀疑代码写错。

Q:动态代理IP的TTL应该设多长?

按代理服务方公示的 IP 存活周期减去 15 到 30 秒缓冲区来设。短效动态代理常见 1 到 5 分钟存活,TTL 设 60 到 180 秒;长效代理常见 15 到 60 分钟,TTL 设 800 到 3300 秒。缓冲区是为了避免"取到手时还剩 5 秒、发出请求时已经失效"的边界情况,这类失败最难排查。

Q:Scrapy的CONCURRENT_REQUESTS和代理IP池大小怎么匹配?

经验值是池大小至少为 CONCURRENT_REQUESTS 的 2 倍,稳妥点做到 3 到 5 倍。因为并发下每个 slot 都可能同时持有一个 IP,同时还有请求正在飞行、IP 正在从池里出队和入队,池太小会导致 process_request 里频繁走同步补池、拉长请求排队时间。

Q:为什么dont_filter=True一定要加?

Scrapy 用请求指纹去重,同一个 URL 默认只调度一次。重试时如果不加 dont_filter=True,重试请求会被去重规则直接拦下,日志里看不到任何异常但请求就是不发出去。这是 Scrapy 代理重试逻辑最容易踩到的隐蔽坑之一。

Q:403和429在代理场景下有什么区别?该怎么区别处理?

403 通常是目标站基于 IP 信誉或访问频率控制机制做出的整体访问限制,往往需要换 IP 并等一段时间才能恢复;429 是显式的限速信号,通常带 Retry-After 头指示重试间隔。中间件里对 429 优先读取 Retry-After 决定延时,对 403 则用固定或指数退避策略。两者都应下架当前 IP,避免连续几次请求打在同一个已被限制的 IP 上。

青果网络代理IP - CTA Banner
点赞(27)
代理IP服务器搭建指南:从环境准备到多出口轮换的完整步骤
代理IP 代理IP配置 住宅IP 企业级代理
2026-09-14

自建代理 IP 服务器与海外 SOCKS5、HTTP 代理如何选型,是数据采集、跨境监测等技术团队常会遇到的难题。本文拆解自建代理能解决与无法解决的问题,讲解 Squid、Dante 搭建、鉴权、系统调优、IP 轮换实操要点,同时对比海外 SOCKS5 和 HTTP 代理的协议差异、DNS 解析逻辑与适用场景,给出完整选型判断流程,帮你避开自建踩坑、协议选错带来的运维风险,快速匹配业务需求。

快代理替换推荐:5家IP代理服务商替代方案实测对比
代理IP选型 国外代理IP IP代理 代理IP配置
2026-09-10

快代理并非每个场景都是最优解。从业务隔离、计费灵活性、稳定性维度出发,青果网络在企业级场景下的适配度更高,极安代理适合中小企业试水,站大爷、巨量代理、熊猫代理各有差异化场景。

代理IP池动态更新频率多久一次?优质服务核心指标揭秘
代理IP IP池 动态ip 代理IP配置
2026-09-09

代理 IP 池不存在统一最优更新频率,需拆分状态检查、资源补充、IP 轮换三类周期,结合业务场景采用定时 + 事件双触发机制。不要只看刷新速度,重点评估补池时延、首次请求成功率、重复率、会话连续性等指标,避免盲目全量刷新带来连接中断、检测成本上升等问题。

AI时代IP池行业会怎么变?6个技术演变方向预测
代理IP池 动态代理IP IP代理 国内代理
2026-09-04

AI 大模型驱动下,IP 池正从人类访问接入通道,转变为面向机器智能的数据供给基础设施。未来三至五年,IP 池将在评估维度、计费模式、合规要求、协议栈、资源形态发生结构性变革。业务适配指标逐步取代 IP 规模成为选型核心,混合定价模式兴起,合规资质成为行业入场券,多协议与多类型 IP 资源并存。本文解析 AI 训练、Agent 推理带来的全新流量特征,对比新老评估体系,梳理供给侧演变方向,并解答行业常见疑问,为企业代理 IP 技术选型提供参考。

返回
顶部