Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月16日

发布于:2026年10月16日

aiohttp 代理配置:会话、身份验证和连接限制

当爬虫需要处理数百个正在处理中的请求,且您希望完全控制事件循环时,aiohttp 便是您的首选客户端。 通过代理进行路由只需几个关键字参数,但有关身份验证、连接限制和超时的默认设置与 `requests` 以及 httpx 不同,这也是大多数 aiohttp 代理配置出错的原因所在。 读完本指南后,您将拥有一个能够通过轮换的住宅网关进行身份验证、运行受限的并行请求、具有可预测的超时行为,并按类型报告代理错误的 `ClientSession`。 示例针对 Python 3.11 或更高版本上的 aiohttp 3.14。若近期版本的行为发生变化,将特别标注版本号。

当爬虫需要处理数百个正在进行中的请求,且您希望完全控制事件循环时,aiohttp 便是您的首选客户端。 通过代理进行路由只需几个关键字参数,但关于身份验证、连接限制和超时的默认设置与 requests 及 httpx 不同,这也是大多数 aiohttp 代理配置出错的原因所在。 读完本指南后,您将拥有一个能够通过轮换的住宅网关进行身份验证、运行受限的并行请求、具有可预测的超时行为,并按类型报告代理错误的 ClientSession。

示例针对 Python 3.11 或更高版本上的 aiohttp 3.14。若近期版本的行为发生变化,将特别标注版本号。

在会话级别配置 aiohttp 代理

proxy 关键字既支持按请求配置,也支持在会话级别配置。按请求配置是原始 API 的实现方式,目前仍可正常使用;而会话级别的配置才是您在构建爬虫时所需的,因为它能彻底杜绝单次调用绕过代理的可能性。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def main() -> None:
    async with aiohttp.ClientSession(proxy=PROXY) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(resp.status, await resp.json())

asyncio.run(main())

端口 9000 是 Geonode 的 HTTP(S) 轮询网关,因此每个新连接都会从不同的住宅 IP 地址发出。 单个调用仍可通过其自身的 proxy= 参数覆盖默认设置,这在某个请求必须直接发送(例如发送到内部指标端点)时非常有用。

按请求设置

async with aiohttp.ClientSession() as session:
    async with session.get("https://api.ipify.org?format=json", proxy=PROXY) as resp:
        print(await resp.json())

当代理在每次调用中发生变化时,请使用此选项,例如当池管理器为每个任务分配不同的粘性会话时。

身份验证:URL 凭据或 proxy_headers

目前支持两种发送代理凭据的方式,此外还有一种已弃用的方式,您在旧版教程中仍会看到。

方法示例状态
代理 URL 中的凭据proxy="http://user:pass@host:9000"受支持
proxy_headers 配合 encode_basic_authproxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}受支持,推荐
proxy_auth=aiohttp.BasicAuth("user", "pass")自 3.14 起已弃用,在 4.0 中移除

URL 形式最为简短。头部形式可将密钥从 URL 中隐藏,若代理 URL 最终出现在日志或异常消息中,这一点尤为重要。aiohttp.encode_basic_auth 会返回现成的 Basic ... 字符串:

import asyncio
import os
import aiohttp

PROXY = "http://premium-residential.geonode.com:9000"

async def main() -> None:
    proxy_headers = {
        "Proxy-Authorization": aiohttp.encode_basic_auth(
            os.environ["GEONODE_USER"], os.environ["GEONODE_PASS"]
        )
    }
    async with aiohttp.ClientSession(proxy=PROXY, proxy_headers=proxy_headers) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

如果您的代码库仍在使用 proxy_auth=aiohttp.BasicAuth(...),它在 3.14 版本中配合 DeprecationWarning 仍可正常工作。请立即迁移:该参数和类将在 aiohttp 4.0 中移除,而替换方案仅需一行代码即可完成。

使用 asyncio.gather 和 Semaphore 进行并行请求

使用 aiohttp 的原因在于并发,而并发会导致代理失效的原因在于,默认情况下没有任何机制对其进行限制。 如果 asyncio.gather 拥有超过一千个协程,它将尝试打开一千个连接;连接器会将此限制在 100 个,其余的请求会在 aiohttp 中排队——这虽比没有好,但无法控制请求的速率。 Semaphore 则明确了该限制,并允许您独立于连接器进行调整。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
CONCURRENCY = 20

async def fetch(session: aiohttp.ClientSession, sem: asyncio.Semaphore, url: str) -> tuple[str, int]:
    async with sem:
        async with session.get(url) as resp:
            await resp.read()
            return url, resp.status

async def main() -> None:
    urls = [f"https://httpbin.org/get?i={i}" for i in range(100)]
    sem = asyncio.Semaphore(CONCURRENCY)
    connector = aiohttp.TCPConnector(limit=CONCURRENCY, ttl_dns_cache=300)
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(
        proxy=PROXY, connector=connector, timeout=timeout
    ) as session:
        results = await asyncio.gather(
            *(fetch(session, sem, u) for u in urls), return_exceptions=True
        )
    ok = sum(1 for r in results if not isinstance(r, Exception))
    print(f"{ok}/{len(urls)} succeeded")

asyncio.run(main())

return_exceptions=True 可防止单个失败请求导致整个批处理被取消。将信号量与连接器的限制值匹配,以确保双方不会默默地将对方无法处理的任务加入队列。

TCPConnector:limit 和 limit_per_host

该连接器管理套接字池。对于 aiohttp 代理流量,有两个参数至关重要。

limit 表示并发连接的总数,默认值为 100。 limit_per_host 将连接限制为一个 (host, port, is_ssl) 三元组,默认值为 0 表示无限制。通过代理时,发往每个目标的每个请求都共享代理的主机和端口,因此每台主机的限制适用于网关,而非目标站点。 对于纯代理工作负载,请将 limit 设置为所需的并发数,并将 limit_per_host 保留为 0;仅当同一会话同时直接与源服务器通信且您希望保护源服务器时,才需设置 limit_per_host。

connector = aiohttp.TCPConnector(limit=30, limit_per_host=10, ttl_dns_cache=300)

ttl_dns_cache=300 将网关的 DNS 响应缓存时间为五分钟(而非默认的十秒),这可消除大多数新连接中的解析器往返过程。会话将独占连接器,并在退出时关闭它;仅当您有意让多个会话共享一个连接器时,才需设置 connector_owner=False。

使用 ClientTimeout 代替默认的 5 分钟

aiohttp 的默认总超时时间为 300 秒。对于直接请求来说,这个时间相当宽裕;但对于代理流量而言则过长——当住宅网关出口出现卡顿时,应尽快报错,以便用户能切换到其他 IP 地址重试。

timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)

total 限制整个请求的时间,包括重定向和请求体下载。connect 限制从连接池获取连接以及 TCP 和代理握手所需的时间。 sock_read 限制连续字节之间的间隔,用于捕获那些接受连接后却以极慢速度传输数据的目标。将该对象传递给会话以采用默认值,或传递给单个请求以覆盖该值。超时会触发 asyncio.TimeoutError,相关内容将在下文讨论。

trust_env 与环境代理

与 requests 不同,除非您通过 trust_env=True 启用该功能,否则 aiohttp 会忽略 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。 启用后,它将使用 urllib.request.getproxies(),遵守 no_proxy 的设置,并从 ~/.netrc 中读取代理凭据(如果存在)。

async with aiohttp.ClientSession(trust_env=True) as session:
    async with session.get("https://api.ipify.org?format=json") as resp:
        print(await resp.json())

对于必须始终使用特定网关的爬虫,请保持 trust_env 关闭,并显式设置 proxy=。这样,CI 运行器上的企业代理就无法劫持路由,no_proxy 也无法悄无声息地豁免某个目标。

SOCKS5 与 aiohttp-socks

aiohttp 本身不支持 SOCKS,因此通过 SOCKS5 运行的 aiohttp 代理需要使用第三方连接器。 aiohttp-socks 软件包提供了一个 ProxyConnector,它取代了 TCPConnector 并负责处理 SOCKS 握手过程。 请与 pip install aiohttp-socks 一起安装。Geonode 的 SOCKS5 轮转端口为 11000:

import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector

SOCKS_URL = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"

async def main() -> None:
    connector = ProxyConnector.from_url(SOCKS_URL)
    async with aiohttp.ClientSession(connector=connector) as session:
        async with session.get("https://api.ipify.org?format=json") as resp:
            print(await resp.json())

asyncio.run(main())

请勿在同一会话中将 ProxyConnector 与 proxy= 结合使用;连接器已负责所有路由工作。ProxyConnector 支持与 TCPConnector 相同的 limit 和 limit_per_host 关键字。

TLS-in-TLS 的限制

aiohttp 会与 HTTP 代理通信,并通过 CONNECT 协议将 https:// 目标设备接入代理隧道。 但它无法可靠地通过 TLS 连接到代理本身(即 https:// 代理 URL),因为 TLS-in-TLS 依赖于 asyncio 传输支持,而该支持在不同 Python 版本中存在差异。文档中明确指出不应对此予以假设。 实际上,这对 Geonode 并不构成问题: 网关是通过 9000/10000 端口的明文 HTTP 连接的,而您的 https:// 流量在 CONNECT 隧道内仍然是端到端加密的,因此请将代理 URL 方案保留为 http://。

多步骤流程中的粘性会话

轮询端口会为每个连接分配一个新的 IP 地址。对于登录流程或分页列表,您需要在几分钟内保持相同的 IP 地址,Geonode 通过用户名在 HTTP(S) 粘性端口 10000 上提供此功能:

import secrets
import aiohttp

def sticky_proxy(country: str = "us", lifetime: int = 600) -> str:
    session_id = secrets.token_hex(4)  # 8 alphanumeric characters
    user = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
    return f"http://{user}:PASSWORD@premium-residential.geonode.com:10000"

session = aiohttp.ClientSession(proxy=sticky_proxy())

会话时长范围为 180 至 86400 秒。州和城市定向无法组合使用,且 asn 要求指定国家。生成新的会话 ID 是按需轮换的方式。

故障诊断:各种异常的含义

异常含义典型解决方法
aiohttp.ClientProxyConnectionError无法与代理主机建立 TCP 连接检查主机名、端口和防火墙;提高 connect 的超时时间
aiohttp.ClientHttpProxyError 伴随 .status == 407代理在 CONNECT 阶段拒绝凭据修正用户名或密码;检查用户名选项的语法
aiohttp.ClientHttpProxyError 伴随 502/503 错误网关无法建立隧道使用新的会话 ID 重试;确认端口与协议匹配
asyncio.TimeoutError某个 ClientTimeout 字段已过期使用新的 IP 地址重试;调整 sock_read
aiohttp.ClientConnectorError通用连接失败(DNS、被拒绝)检查 .os_error

ClientProxyConnectionError 是 ClientConnectorError 的子类,ClientHttpProxyError 是 ClientResponseError 的子类,因此应优先捕获具体类型。

import asyncio
import aiohttp

PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"

async def fetch_ip(session: aiohttp.ClientSession) -> str | None:
    try:
        async with session.get("https://api.ipify.org?format=json") as resp:
            resp.raise_for_status()
            return (await resp.json())["ip"]
    except aiohttp.ClientHttpProxyError as e:
        if e.status == 407:
            print("proxy auth failed: check credentials")
        else:
            print("proxy returned", e.status)
    except aiohttp.ClientProxyConnectionError as e:
        print("cannot reach proxy:", e)
    except asyncio.TimeoutError:
        print("timed out through proxy")
    return None

async def main() -> None:
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with aiohttp.ClientSession(proxy=PROXY, timeout=timeout) as session:
        print(await fetch_ip(session))
    await asyncio.sleep(0.25)  # let SSL transports close before the loop stops

asyncio.run(main())

请注意,对于普通的 http:// 目标,代理返回的 407 状态码会作为普通响应而非异常出现,因此也需检查 resp.status。

优雅关闭

当会话关闭时,asyncio 可能仍在拆除底层的 SSL 传输连接。如果事件循环先停止,就会出现 ResourceWarning: unclosed transport 错误。 文档中提到的解决方法是在会话的 async with 代码块之后短暂暂停:对于普通 HTTP 会话,应使用 await asyncio.sleep(0);而当会话发出 HTTPS 请求时(通过 CONNECT 隧道,这种情况几乎总是存在),则应使用 await asyncio.sleep(0.25)。上面的示例中已包含这一处理。

常见错误

每个请求创建一个会话

ClientSession 管理连接池和 Cookie 存储。如果每个请求都打开一个会话,不仅会破坏与网关的保持活动连接机制,还会增加代理握手次数,并导致每次调用时触发关闭警告。应为每个工作进程创建一个会话,并在进程间传递使用。

忽略代理是唯一的“主机”这一事实

当 limit_per_host 设置为较小数值时,每个经过代理的请求都会计入网关的单个 (host, port) 条目中,因此“每主机 5 个”的限制会将整个爬虫的连接数限制为 5 个。 请改设 limit,或将 limit_per_host 保留为 0。

依赖默认超时设置

五分钟的时间足够长,以至于一个卡住的住宅出口服务器会占用信号量槽位整个超时窗口。请务必在传递 ClientTimeout 时一并提供 connect 和 sock_read 的值。

仍传递 proxy_auth

当前版本中该操作会触发警告但仍可运行,但在 4.0 版本中将无法工作。请将其替换为 proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth(user, pw)},或将凭据放入代理 URL 中。

下一步

通过上面的操作,您已经对 aiohttp 代理流量进行了身份验证、绑定和可观察化;剩下的就是它所经过的代理池。Geonode 住宅代理 为此处使用的端点提供服务: 250 万个在线 IP,每月在 195 多个国家/地区轮换 3000 万至 5000 万个独立 IP,可通过用户名实现国家、州、城市及 ASN 定向,并在 HTTP、HTTPS 和 SOCKS5 上支持长达 24 小时的粘性会话。 批量采购起价为 0.27 美元/GB,入门套餐为 10 GB/月,价格 7.92 美元,3 天试用期费用为 5 美元,无需签订合同。 网页抓取 和 价格情报 用例展示了团队如何将此连接器配置与重试和调度逻辑相结合。

常见问题

如何为整个会话设置 aiohttp 代理?

将 proxy= 传递给 ClientSession 的构造函数:aiohttp.ClientSession(proxy="http://user:pass@host:port")。除非调用时用其他值覆盖 proxy=,否则该会话中的每个请求都会使用它。 会话级代理自 aiohttp 3.11 起已支持;在较早的版本中,您必须在每次调用 get() 或 post() 时传入 proxy=。

为什么 aiohttp 中 proxy_auth 已被废弃,以及有什么替代方案?

自 aiohttp 3.14 起,proxy_auth 参数和 aiohttp.BasicAuth 类已被废弃,并计划在 4.0 版本中移除。 其替代方案是一个简单的标头:proxy_headers={"Proxy-Authorization": aiohttp.encode_basic_auth("user", "pass")}。它无需辅助类即可实现相同的 Base64 编码,并且既可在单次请求中使用,也可在会话级别使用。

aiohttp 是否支持 HTTPS 代理或 SOCKS5?

aiohttp 原生支持与 HTTP 代理通信,并使用 CONNECT 协议将 https:// 目标通过代理进行隧道传输。通过 TLS 连接到代理本身(TLS-in-TLS)取决于您的 Python 构建中是否支持 asyncio,因此不应依赖此功能。 SOCKS4 和 SOCKS5 需要第三方 aiohttp-socks 包及其 ProxyConnector 组件。

通过 aiohttp 代理最多可以运行多少个并发请求?

TCPConnector 默认允许 100 个并发连接,且无单个主机连接数上限。 通过代理时,每个请求都会共享相同的主机和端口,因此请显式设置 limit;若同时进行直接调用,请添加 limit_per_host;并将请求协程封装在 asyncio.Semaphore 中,以避免队列中的任务同时打开套接字。

为什么通过代理发起的 aiohttp 请求会挂起而不是失败?

ClientSession 默认设置为 5 分钟的总超时,当住宅出口服务器卡顿时,这看起来就像是挂起。 将 aiohttp.ClientTimeout(total=30, connect=10, sock_read=20) 传递给会话或请求,这样卡顿会迅速以 asyncio.TimeoutError 的形式显现,随后使用新的会话 ID 重试,而不是一直等待。

通过住宅代理路由爬虫有哪些实际限制?

代理仅会更改源 IP。 它不会授予忽略 robots.txt、网站条款或速率限制的权限,也不会使收集个人数据的行为合法化。请保持合理的并发量,在遇到 429 状态码时进行退避,并记住 Geonode 粘性会话最长持续 24 小时,之后 IP 地址将被释放。