一、为什么异步爬虫需要代理?
同步爬虫(如 requests)在并发量上来后,往往需要开大量线程,资源消耗大且效率低。aiohttp 基于 asyncio,单线程即可处理数百个并发请求,非常适合大规模数据采集。
但大规模请求会带来两个问题:
-
单 IP 请求过于集中,容易触发目标网站的频率管理机制。
-
网络链路不稳定,单个请求超时可能拖慢整个任务。
代理 IP 可以分散请求来源,提高采集的稳定性和成功率。而 aiohttp 的代理配置与同步库不同,需要正确设置连接池和超时。
二、aiohttp 代理基础配置
aiohttp 通过 session.get() 或 session.post() 的 proxy 参数指定代理地址。代理地址格式为 http://user:pass@host:port。
python
import aiohttp
import asyncio
PROXY = "http://user:pass@your-proxy-gateway.com:8080"
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url, proxy=PROXY, timeout=aiohttp.ClientTimeout(total=10)) as resp:
return await resp.text()
async def main():
html = await fetch("https://httpbin.org/ip")
print(html)
if __name__ == "__main__":
asyncio.run(main())
如果需要认证,推荐使用 aiohttp.BasicAuth:
python
import aiohttp
PROXY_URL = "http://your-proxy-gateway.com:8080"
PROXY_AUTH = aiohttp.BasicAuth("username", "password")
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) as resp:
return await resp.text()
对于 SOCKS5 代理,需要安装 aiohttp-socks,使用 ProxyConnector:
python
from aiohttp_socks import ProxyConnector
import aiohttp
async def fetch_with_socks(url):
connector = ProxyConnector.from_url("socks5://user:pass@your-proxy-gateway.com:1080")
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get(url) as resp:
return await resp.text()
三、连接池复用:减少握手开销
aiohttp.ClientSession 内部维护连接池。每次请求都新建 Session 是性能杀手,因为每次都要重新建立 TCP 连接和 TLS 握手。
正确做法:整个采集周期复用一个 Session,并配置 TCPConnector。
python
import aiohttp
import asyncio
async def main():
connector = aiohttp.TCPConnector(
limit=200, # 总连接数上限
limit_per_host=20, # 每个主机最大连接数
keepalive_timeout=30, # 空闲连接保持 30 秒
ttl_dns_cache=300, # DNS 缓存 5 分钟
ssl=False, # 测试环境可关闭 SSL 验证
)
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=15)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
tasks = [fetch(session, url) for url in urls]
await asyncio.gather(*tasks)
async def fetch(session, url):
async with session.get(url, proxy=PROXY) as resp:
return await resp.text()
关键参数说明:
-
limit:整个连接池的最大连接数。 -
limit_per_host:每个目标主机的最大连接数。对于代理网关,每个代理地址算一个主机。 -
keepalive_timeout:空闲连接保持时间,太长会占用资源,太短会频繁重建。建议 10--30 秒。 -
ttl_dns_cache:DNS 缓存时间,减少解析开销。
连接复用陷阱 :同一个 IP 高频复用 TCP 连接可能被识别为异常。建议将 keepalive_timeout 设为 5--10 秒,或定期更换代理。
四、超时控制:避免慢请求拖垮并发
aiohttp.ClientTimeout 提供四个超时字段:
| 字段 | 说明 | 推荐值 |
|---|---|---|
total |
整个请求的总超时 | 30 秒 |
connect |
建立连接(TCP + TLS)超时 | 10 秒 |
sock_connect |
TCP 连接超时 | 10 秒 |
sock_read |
两次数据读取之间的最大间隔 | 15 秒 |
最容易混淆的是 sock_read :它计算的是两次 socket read 之间的间隔,而不是整个响应读取时间。如果服务器先返回响应头,然后 20 秒后才返回 body,sock_read=15 会触发超时。
代理场景下,connect 阶段涉及"客户端→代理→目标网站"的多段握手,因此 connect 需要比直连场景调大一些。
python
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(
total=30,
connect=10,
sock_read=15,
)
必须设置 total,否则慢请求会长期占用连接池,导致后续请求排队。
五、完整实战:轮换代理 + 并发控制
下面是一个完整的异步爬虫示例,使用轮换代理、连接池复用、超时控制,并限制并发数。
python
import asyncio
import aiohttp
import random
from typing import Optional
PROXIES = [
"http://user:pass@your-proxy-gateway.com:8080",
"http://user:pass@your-proxy-gateway.com:8081",
"http://user:pass@your-proxy-gateway.com:8082",
]
class AsyncProxyCrawler:
def __init__(self, proxies, concurrency=20):
self.proxies = proxies
self.semaphore = asyncio.Semaphore(concurrency)
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
connector = aiohttp.TCPConnector(
limit=200,
limit_per_host=10,
keepalive_timeout=10,
ttl_dns_cache=300,
ssl=False,
)
self.session = aiohttp.ClientSession(
connector=connector,
timeout=aiohttp.ClientTimeout(total=30, connect=10, sock_read=15),
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
},
)
return self
async def __aexit__(self, *args):
if self.session:
await self.session.close()
def _pick_proxy(self):
return random.choice(self.proxies)
async def fetch(self, url: str, retries: int = 3) -> Optional[str]:
for attempt in range(retries):
proxy = self._pick_proxy()
try:
async with self.semaphore:
async with self.session.get(url, proxy=proxy) as resp:
if resp.status == 200:
return await resp.text()
if resp.status in (429, 503):
await asyncio.sleep(2 ** attempt)
continue
return None
except asyncio.TimeoutError:
continue
except aiohttp.ClientError:
continue
return None
async def main():
urls = [f"https://httpbin.org/ip?idx={i}" for i in range(50)]
async with AsyncProxyCrawler(PROXIES, concurrency=10) as crawler:
tasks = [crawler.fetch(url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
success = sum(1 for r in results if isinstance(r, str))
print(f"成功: {success}/{len(urls)}")
if __name__ == "__main__":
asyncio.run(main())
代码要点:
-
复用
ClientSession。 -
TCPConnector控制连接池。 -
ClientTimeout分层超时。 -
asyncio.Semaphore限制并发。 -
轮换代理,失败重试。
六、常见问题(FAQ)
Q:aiohttp 支持 SOCKS5 代理吗?
A:原生不支持,需要安装 aiohttp-socks,使用 ProxyConnector。
Q:为什么设置了 proxy 参数,请求还是走本地 IP?
A:检查代理地址格式是否正确(需带 http://),认证信息是否通过 proxy_auth 独立传递,以及环境变量是否覆盖了配置。
Q:sock_read 和 total 有什么区别?
A:sock_read 是两次数据读取之间的最大间隔,total 是整个请求的总超时。必须同时设置。
Q:连接池的 limit_per_host 对代理场景有什么影响?
A:如果使用网关式代理,所有请求都发往同一个网关地址,limit_per_host 限制的是通过该网关的最大并发。需要根据网关承载能力设置。
Q:如何选择轮换会话和粘性会话?
A:批量独立请求用轮换,需要登录态或连续翻页用粘性。B2Proxy 的住宅代理支持地区和 ASN 定向,并提供轮换与粘性会话选项,可以根据任务类型灵活配置。
总结
aiohttp 异步爬虫的代理配置,核心在于三点:
-
代理配置 :用
proxy参数传地址,用proxy_auth传认证。SOCKS5 需要aiohttp-socks。 -
连接池复用 :复用
ClientSession,合理设置TCPConnector的limit、limit_per_host、keepalive_timeout。 -
超时控制 :使用
ClientTimeout分层超时,total必须设置,sock_read不要过短。
把这三点做好,异步爬虫的稳定性和效率都会有明显提升。对于需要多地区采集的任务,可以选择支持地区和 ASN 定向的住宅代理服务,配合轮换与粘性会话,进一步提升数据采集的稳定性。