实操教程2026年9月29日阅读约 7 分钟

处理封锁和重试,别白白烧掉代理流量

在按 GB 计费的代理上,一个粗心的重试循环会为同一个封锁页一遍遍付钱。哪些失败该重试,怎么退避,以及怎么提前停止下载。

一个在你笔记本上看着无害的重试循环,可能悄悄变成按 GB 计费账单上最大的一项。常见的罪魁祸首是一个开始用封锁页回应你的网站:每次尝试都下载同样几百 KB 的「请证明你是真人」,而循环还在不停地问。

这篇讲的是只把流量花在你想要的响应上。它默认你用的是按 GB 计费的产品,比如我们的住宅代理;在按 IP 计费的线路上,同样的习惯能省时间,也能让你的 IP 保持更好的名声。

一次失败的尝试要花多少

取决于回来的是什么。

结果 网站有正文返回吗? 在我们的用量表上
超时、连接重置、我们网关返回的错误(比如 407 或 502) 没有 免费,日志里可见
网站返回的 429、5xx 通常很小 和任何响应一样计量
403 页面、验证码、“请证明你是真人” 有 和任何响应一样计量
装着封锁页的 200 有 和任何响应一样计量

只有第一行免费:连接失败了,网站那边什么都没穿过代理。其余几行都是网站在回应你,这些字节就是流量。对 HTTPS 网站来说,状态码走在加密隧道里面,我们的代理读不到,所以用量表数的是穿过隧道的字节,分不出 429 和 200。规则见计量部分。

429 或 503 通常是很短的响应,只有一个正常页面的零头,所以好好等一段时间再重试,花不了多少。封锁页要重得多,一遍又一遍去抓封锁页的循环,才是重试烧钱的地方。

只重试会变的

决定再试一次之前,先把响应分类:

  • 退避后重试:超时、连接错误、429、502、503、504。这些是暂时性的,或者是网站让你等一等。每收到一个 429 或 5xx,都是一小笔计量的响应,所以两次尝试之间要等够,也要给重试次数设上限。
  • 换个新 IP 重试一次,然后停:403 和明显的封锁页。有轮换的话,在新地址上再试一次是合理的。第二次还被封,说明问题出在你的请求、你的频率或你的指纹上,排查清单见爬虫为什么突然开始被封了。
  • 不要重试:404、410、400。答案不会变。

退避,并遵守 Retry-After

带一点随机性的指数退避,能防止一池子工作进程齐步重试。网站发来 Retry-After 时,它已经告诉你该等多久了。

import random
import time


def backoff(attempt):
    return min(60, 2 ** attempt) * random.uniform(0.5, 1.0)


def retry_after(response):
    value = response.headers.get("Retry-After", "")
    return int(value) if value.isdigit() else None

提前停止下载

用了 stream=True,requests 会先读取状态行和响应头,等你要正文时才去取。这给了你两个省钱的出口:在正文到达之前就拒绝你不想要的状态码;读第一块数据,在下载剩余部分之前认出封锁页。提前关闭响应,意味着不想要的正文大部分根本不会穿过代理。

import requests

PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
PROXIES = {"http": PROXY, "https": PROXY}
RETRYABLE = {429, 502, 503, 504}
BLOCK_MARKERS = (b"captcha", b"verify you are human", b"access denied")
MAX_BYTES = 3_000_000


def fetch(url, attempts=4):
    blocked_once = False
    for attempt in range(attempts):
        try:
            response = requests.get(url, proxies=PROXIES, timeout=30, stream=True)
        except (requests.ConnectionError, requests.Timeout):
            time.sleep(backoff(attempt))
            continue

        with response:
            if response.status_code in RETRYABLE:
                time.sleep(retry_after(response) or backoff(attempt))
                continue
            if response.status_code == 403:
                if blocked_once:
                    return None
                blocked_once = True
                continue
            if response.status_code != 200:
                return None
            if int(response.headers.get("Content-Length") or 0) > MAX_BYTES:
                return None

            chunks = response.iter_content(65_536)
            first = next(chunks, b"")
            if any(marker in first.lower() for marker in BLOCK_MARKERS):
                if blocked_once:
                    return None
                blocked_once = True
                continue
            return first + b"".join(chunks)
    return None

BLOCK_MARKERS 要从目标站实际返回的封锁页里挑;在浏览器里打开一个,复制其中一句话。中文网站的封锁页,就把对应的中文短语编码成字节放进去。HOST、PORT、USERNAME 和 PASSWORD 来自我们控制台里该服务的页面。

整个站点都在拒绝时,先停下

按 URL 的重试注意不到一个站点上所有 URL 都在失败。为每个站点保留一个最近结果的短窗口,当其中大多数都是封锁时,暂停一段时间不再发送。

from collections import defaultdict, deque

recent = defaultdict(lambda: deque(maxlen=20))


def record(host, blocked):
    recent[host].append(blocked)


def should_pause(host):
    window = recent[host]
    return len(window) == window.maxlen and sum(window) > window.maxlen // 2

暂停不花钱。同一段时间里一百个工作进程在下载封锁页,就是账单上实实在在的一项。

一开始就少要一些字节

  • 保持压缩开启。 多数 HTTP 客户端默认会发送 Accept-Encoding: gzip。确认一下你的也是。
  • 对会回访的页面用条件请求。 把上次看到的 ETag 作为 If-None-Match 发出去;没变的页面会以不带正文的 304 返回。
  • 优先用页面自己加载的 JSON。 网站自己的数据接口,通常只有渲染出来的页面的一小部分。
  • 在无头浏览器里,屏蔽你用不到的东西。 图片、字体和媒体文件,常常比 HTML 本身重上很多倍。
etags = {}


def fetch_if_changed(url):
    headers = {"If-None-Match": etags[url]} if url in etags else {}
    response = requests.get(url, headers=headers, proxies=PROXIES, timeout=30)
    if response.status_code == 304:
        return None
    if "ETag" in response.headers:
        etags[url] = response.headers["ETag"]
    return response.content

想看看这些到底省了多少,就在改动前后分别拿你自己数的字节和用量表对比;脚本在按 GB 计费怎么算钱里。关于某个具体目标站的问题,欢迎带到 Discord。

还有后续问题?

去 Discord 问。答案还能帮到下一个读到这个帖子的人。

加入 Discorddiscord.gg/proxypanda
$5 起步去 Discord 问