aiohttp 能通过异步 I/O 同时处理大量网络任务,但并发提高后,代理出口很容易成为新的瓶颈:一个代理被多个协程同时占满、连接池排队时间过长、网络异常与 HTTP 状态码混在一起,都会让失败率快速上升。

本文实现一套适用于授权数据采集、API 测试和多地区页面质量检查的代理轮换方案。这里的轮换是多个合规网络出口之间的负载分配与故障切换,不用于破坏验证流程或规避目标站点规则。

自动化采集前,应确认服务条款、robots.txt、接口授权和频率限制。代理只能改变网络出口,不能替代访问许可。

一、aiohttp代理配置的关键点

根据 aiohttp 3.14.1 官方文档:

  • 普通 HTTP 代理可通过请求的 proxy 参数设置;
  • 访问 HTTPS 目标时,通常通过 HTTP CONNECT 建立隧道;
  • aiohttp 默认不读取 HTTP_PROXYHTTPS_PROXY 等环境变量;
  • 需要环境变量时,应显式设置 ClientSession(trust_env=True)
  • proxy_auth 已弃用,3.14 推荐使用 encode_basic_auth() 生成代理认证头;
  • SOCKS 不属于 aiohttp 原生代理能力,需要第三方连接器。

真实项目不应为每个请求创建 ClientSession。Session 内部维护连接池,应在一批任务中复用。

二、代理轮换需要控制三种容量

高并发代理池不是随机选择一个 IP,而是同时限制:

  1. 连接池总容量:由 TCPConnector(limit=...) 控制;
  2. 单个代理容量:防止所有协程集中到一个出口;
  3. 目标域名并发:即使代理很多,也要遵守目标接口的频率要求。

本文使用代理令牌队列:每个令牌代表一个并发请求,请求结束后归还队列。

三、最小可运行代码

安装依赖:

python -m pip install "aiohttp>=3.14,<4"
import asyncio

import aiohttp


async def fetch(
    session: aiohttp.ClientSession,
    queue: asyncio.Queue[str],
    url: str,
) -> dict[str, object]:
    proxy = await queue.get()
    proxy_name = proxy.split("@")[-1]

    try:
        async with session.get(url, proxy=proxy) as response:
            await response.content.read(1024)
            return {
                "proxy": proxy_name,
                "status": response.status,
                "retry_after": response.headers.get("Retry-After"),
            }
    except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
        return {
            "proxy": proxy_name,
            "status": None,
            "error": type(exc).__name__,
        }
    finally:
        queue.put_nowait(proxy)


async def main() -> None:
    queue: asyncio.Queue[str] = asyncio.Queue()
    proxies = [
        "http://USER:PASSWORD@proxy-a.example.com:8000",
        "http://USER:PASSWORD@proxy-b.example.com:8000",
    ]

    # 每个代理两个令牌,单代理最多并发两个请求。
    for proxy in proxies:
        for _ in range(2):
            queue.put_nowait(proxy)

    timeout = aiohttp.ClientTimeout(
        total=30,
        connect=8,
        sock_connect=5,
        sock_read=20,
    )
    connector = aiohttp.TCPConnector(
        limit=16,
        limit_per_host=8,
        ttl_dns_cache=300,
    )
    urls = ["https://httpbin.org/status/200"] * 8

    async with aiohttp.ClientSession(
        connector=connector,
        timeout=timeout,
        headers={"User-Agent": "AuthorizedCollector/1.0"},
    ) as session:
        results = await asyncio.gather(
            *(fetch(session, queue, url) for url in urls)
        )

    for result in results:
        print(result)


if __name__ == "__main__":
    asyncio.run(main())

四、代码中的关键设计

1. 令牌队列限制单代理并发

每个代理放入两个相同令牌,最多允许两个协程同时使用该出口。请求完成或抛出异常后,finally 都会归还令牌。

2. 复用ClientSession

ClientSession 保存连接池和 Cookie。批次内复用 Session 可以减少重复建连,并避免频繁创建 Session 导致资源警告。

3. 分层设置超时

total 控制请求总时间,connect 包含等待连接池的时间,sock_connectsock_read 分别控制建连与读取阶段。连接超时也可能来自连接池排队,并不一定说明代理不可达。

4. HTTP响应和网络错误分开处理

ClientError 与超时会写入 error;403、429、5xx 则保留为 HTTP 状态码。429 应按 Retry-After 降速,403 应检查权限、路径和会话,不能直接判断代理失效。

5. 连续会话固定出口

分页、Cookie 或连续事务不应逐请求轮换。可以在任务组开始时从队列取出一个代理,所有分页完成后再归还。

五、健康检查与轮换规则

情况 处理建议
单次传输错误 记录并有限重试
连续传输错误 进入冷却,稍后健康检查
403 检查权限、路径和会话
429 按Retry-After降速
5xx 有限退避,不直接淘汰代理
地区不符合预期 从对应地区池移除
分页或连续会话 任务组内固定出口

六、部署与安全建议

代理账号和密码不应写入源码。建议通过环境变量或密钥管理服务注入,日志只记录代理编号,不输出完整代理 URL、Cookie 和认证头。

需要地区选择、会话保持和稳定出口时,可在工具评估阶段测试网络出口配置的协议兼容、并发容量、地区准确性、P95 延迟和故障响应能力。正式接入前应使用自己的授权接口进行小流量验证。

不要通过 ssl=False 长期关闭证书校验。使用自定义 CA 时,应创建正确的 ssl.SSLContext。大量协程也不等于需要同等数量的连接。

总结

aiohttp 高并发代理调度的重点不是频繁换 IP,而是分别控制连接池总容量、单代理容量和目标域名并发。

轮换只应用于任务分配、地区路由和传输故障;403、429 与解析失败则分别检查权限、频率和页面结构。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐