跳转至

中间件与代理

下载中间件

在下载前后统一处理所有请求(加签名、换 Cookie、统计等)。

from netspy.network.middleware import DownloaderMiddleware


class SignMiddleware(DownloaderMiddleware):
    def process_request(self, request):
        request.params = {**request.requests_kwargs.get("params", {}), "sign": sign(request.url)}
        return request                    # 返回 Response 则短路下载

    def process_response(self, request, response):
        if response.status_code == 202:
            return request                # 返回 Request 则丢回队列重新调度
        return response

setting.py

DOWNLOADER_MIDDLEWARES = ["myproj.middlewares.SignMiddleware"]

process_request 按列表顺序执行,process_response 逆序。爬虫自己的 download_midware 方法仍然有效,在全局中间件之后执行。

代理池

PROXY_ENABLE = True
PROXY_EXTRACT_API = "http://proxy-provider.com/get?count=10"   # 返回每行一个代理,或 JSON 数组
PROXY_MAX_USE_TIMES = 100      # 单个代理用满这么多次后轮换
PROXY_WAIT_TIMEOUT = 30.0      # 池空时最多等多久,超时抛 ProxyUnavailableError
PROXY_ALLOW_DIRECT = False     # True 才允许「没代理就直连」

HttpxDownloader 会在请求没有显式代理时从池里取一个;下载报错时自动 report_bad 丢弃该代理。

拿不到代理时不会直连

代理用满 PROXY_MAX_USE_TIMES、被 report_bad 丢弃、或取号接口断供, 都会让池空掉。这时框架不会退回直连 —— 那会把源 IP 暴露给目标站, 而开代理池的全部意义就是别这么干。

池空时先等 PROXY_WAIT_TIMEOUT(默认 30 秒)并按 PROXY_MIN_INTERVAL 的节奏重新取号;仍拿不到才抛 ProxyUnavailableError。 该请求走正常重试,重试用尽后落到 failed_requests.jsonlnetspy retry --requests 只是探活(重新下载看状态码,不跑回调、不入库);要把数据真正抓回来,开 RETRY_FAILED_ON_START 重跑一次爬虫。

这个错误不计入熔断 —— 代理供应是自己这边的问题, 算进去的话代理商断供五分钟就能把所有域全熔断一遍。

确实想要「有代理就用、没有就直连」,把 PROXY_ALLOW_DIRECT = True 打开 —— 显式配置就不算静默。

早先的版本在这里返回 None,请求就那样直连出去了:实测一次断供后 21 个请求里有 16 个从本机 IP 打到了靶子,没有任何提示。

池空时(且距上次拉取超过 PROXY_MIN_INTERVAL)重新拉取。

自定义代理池:继承 netspy.network.proxy_pool.base.ProxyPool,实现 get_proxy(), 然后 PROXY_POOL = "myproj.MyProxyPool"。另外三个是可选钩子,不实现也不会出错:

钩子 什么时候被调
report_bad(proxy) 确定是代理的错:连不上代理、CONNECT 被拒
report_suspect(proxy) 说不清是谁的错:读超时、连接重置、响应畸形
report_good(proxy) 这个代理刚成功完成一次请求(每个成功请求都调,实现要便宜)

代理用满次数之后会轮换回来

PROXY_MAX_USE_TIMES 是「用满这么多次后轮换」,不是终身配额:代理被丢出池、 下次拉取时补回来,使用次数随之清零,可以重新用满一轮。

v4.48 之前不是这样

计数从不清零,于是补回池里的代理再用一次就又被丢。后果只在第 PROXY_MAX_USE_TIMES × 池大小 个请求之后才出现,短测试永远看不见 —— 实测 3 个代理、PROXY_MAX_USE_TIMES=5 的池,第一轮用完后稳态退化成 每秒只取到 3 个代理(其余全部空手而归),并且每秒去拉一次供应商的 列表接口,永远不停。修好后同一配置是每秒 15 个。

配合 PROXY_ALLOW_DIRECT=False(默认),那些空手而归的请求会各自等满 PROXY_WAIT_TIMEOUT(默认 30 秒)再失败。

同一批修复还回收了按代理做键的记账。很多代理商的提取接口每次返回全新 IP, 而 _use_count / _banned / _fails / _suspects 原先只增不减 —— 实测 3000 次 get_proxy 之后 _use_count 里躺着 1500 条。现在每次拉取列表前 回收一次,只做语义等价的回收:过期的冷却、超过 PROXY_BAN_MAX_SECONDS 的失败记录(现有逻辑本来就当它们已归零)、以及既不在池中也不在冷却中的 使用次数。退避阶梯不动 —— 有一条用例专门盯着这一点。

不是所有失败都算代理的错

下载器原先把任何下载异常都当成代理故障。后果实测过:三个完全健康的代理 (各自都成功连上并转发了请求)配一个 5 秒才回的目标站、1 秒超时 —— 三个请求就把整池清空,之后每个请求都要等满 PROXY_WAIT_TIMEOUT(默认 30 秒) 才失败。一个慢 URL 让整个爬虫停摆一分钟。

现在按阶段分流:

  • 连接阶段失败ConnectError / ConnectTimeout / ProxyError,curl 的 CURLE_COULDNT_CONNECT / COULDNT_RESOLVE_PROXY / PROXY)—— 配了代理时 TCP 的对端就是代理本身,连不上只可能是它的问题:立刻拉黑,行为不变。
  • 连上之后的失败(读超时、连接重置、响应畸形)—— 说不清是谁的错,多半是 目标站:记一次「可疑」,连续攒够 PROXY_SUSPECT_BAN_AFTER(默认 3)次才拉黑。 中间成功一次就清零 —— 真挂掉的代理会连续失败,照样被拉黑,只是晚几次。

顺带修正了一句假话

退避日志里那句「第 N 次连续失败」此前是假的:代理池从来不知道 「成功」这回事,_fails 只在距上次失败超过 PROXY_BAN_MAX_SECONDS(900 秒) 时才清零。也就是说一个跑了一万次成功、15 分钟内偶尔失败三次的代理 会被退避到 4 倍冷却。加上 report_good 之后,那句话才名副其实。

升级前写的自定义代理池不受影响:三个新钩子都是可选的,没实现就跳过 —— 对 report_suspect 而言「跳过」正好等于「目标站的锅不算代理头上」。

单个请求也可指定:mw.Request(url, proxy="http://user:pass@host:port")