代理池、限速与请求指纹

限速是对目标站点的基本礼貌,也是保护自己的手段;代理与指纹一致性的讨论,前提是"合规地分散自建出口的负载",而不是"隐藏身份继续高频抓取"。本章给可落地的限速实现、代理的使用边界与健康检查,以及一张能直接抄的配置表。

为什么必须限速

两个理由权重相同:对别人(不因为你的采集影响正常用户)、对自己(稳定的低速任务比忽快忽慢的高频任务更容易跑完)。

不限速的后果具体表现限速后的收益
触发限流429、响应变慢、返回空数据数据完整度稳定
被临时封禁403、IP 段被拉黑、账号受限任务可长期无人值守运行
影响对方业务高峰期响应变慢,容易引发投诉大幅降低被投诉与被封的概率

令牌桶:控制平均速率,允许小幅突发

漏桶严格匀速,令牌桶允许攒够令牌后连发几次。对第三方站点,建议直接用漏桶式的固定间隔。

import threading
import time

class TokenBucket:
    """令牌桶:平均 rate 个/秒,容量 burst,线程安全。"""

    def __init__(self, rate: float, burst: int = 1) -> None:
        self.rate = rate
        self.capacity = max(burst, 1)
        self._tokens = float(self.capacity)
        self._updated_at = time.monotonic()
        self._lock = threading.Lock()

    def acquire(self, tokens: int = 1) -> None:
        while True:
            with self._lock:
                now = time.monotonic()
                self._tokens = min(self.capacity, self._tokens + (now - self._updated_at) * self.rate)
                self._updated_at = now
                if self._tokens >= tokens:
                    self._tokens -= tokens
                    return
                wait = (tokens - self._tokens) / self.rate
            time.sleep(min(wait, 0.5))  # 短暂放锁,避免长时间持有

并发上限:信号量比"心里有数"靠谱

并发与速率是两个闸门,缺一不可;连接池大小与信号量保持一致,避免"限了并发却开了一堆连接":

import asyncio

import aiohttp  # pip install aiohttp

sem = asyncio.Semaphore(4)  # 同一域名最大并发
connector = aiohttp.TCPConnector(limit=4, limit_per_host=4)

async def fetch(session: aiohttp.ClientSession, url: str) -> str:
    async with sem:               # 并发闸门
        await asyncio.sleep(0.5)  # 速率闸门
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
            resp.raise_for_status()
            return await resp.text()

代理的作用与使用边界

代理解决的是"单一出口 IP 负载过于集中",它不解决"你不该抓"这件事。

场景是否合适说明
自建代理分摊多台机器的出口合适自己控制带宽与日志,行为可审计
公司统一出口被限流,换自建出口合适先与站点沟通,再调整出口
用商业代理池隐藏高频抓取不合适只是转移风险,行为性质未变
用代理绕过地区限制访问付费内容违法绕过访问控制,可能涉及刑事责任
用代理访问隐私数据源违法与是否用代理无关,行为本身即违法

判断标准很简单:如果你需要向对方解释"为什么不能知道是我在请求",那这件事就不该做。

代理池的健康检查与轮换

import random

import requests

def check_proxies(proxies: list[str], check_url: str, timeout: float = 5.0) -> list[str]:
    """逐个探测代理可用性,只返回可用的那部分。"""
    alive = []
    for proxy in proxies:
        try:
            resp = requests.get(check_url, proxies={"http": proxy, "https": proxy}, timeout=timeout)
            if resp.ok:
                alive.append(proxy)
        except requests.RequestException:
            continue  # 不可用直接剔除,不做无谓重试
    return alive

def pick(alive: list[str]) -> str | None:
    """从可用代理中轮换取一个;返回 None 时调用方应回退直连并降频。"""
    return random.choice(alive) if alive else None

请求指纹的一致性

指纹不一致本身就是异常信号:UA 说是 Windows 上的 Chrome,Accept-Language 却是别的语种,时区对不上,连接行为又像脚本,组合起来比高频更容易被拦。

维度一致的做法不一致的后果
User-Agent全流程固定同一个自定义 UA每次请求换 UA 是典型脚本特征
Accept-Language与访问内容的语言匹配与地区、UA 矛盾时易被判异常
Cookie同一会话内保持一致一半带一半不带,像多客户端混用
连接复用Session 复用 TCP 连接每次新建连接,握手特征过于突出
请求节奏稳定间隔,允许轻微波动毫秒级等间隔是最明显的时间指纹
出口 IP一个任务固定一个出口同一会话频繁换 IP 会触发风控

可直接抄的限速配置表

目标单域名并发请求间隔每小时上限失败处理
小型个人站点13 秒约 1000失败即停,人工确认
一般公开站点21~2 秒约 2000指数退避,最多 3 次
大型站点 + 官方 API Key按配额按配额按配额按官方文档的重试策略
自己可控的服务压测决定不限不限在压测目标内自由调整

常见坑

说明
只限并发不限速率并发 4 也能在一秒内打出几十个请求
代理池不做健康检查大量时间耗在不可用代理上,整体更慢
用代理代替降频换 IP 继续高频,仍然在影响对方服务
为"更像人"而随机 sleep随机不等于合理,稳态低速比忽快忽慢更好
忽略 Retry-After站点已给出等待时间,硬顶只会换来更长的封禁

小结:限速对站点和对自己都是最划算的投入,用令牌桶控速率、信号量控并发,代理只用于自建与合规场景并配合健康检查,请求指纹要保持前后一致;能靠降频解决的,不要用代理去绕。

笔记加载中…