Day 132 - 代理池搭建
阶段:Phase 7 - 进阶与性能优化 · 实战
主题:免费/付费代理获取、代理验证与可用性检测、代理池架构、高匿名代理池实战
一、概念解释
1.1 什么是代理(Proxy)
代理是一台位于爬虫与目标服务器之间的"中转服务器"。爬虫的请求先发给代理,代理再转发给目标网站;网站的响应也经由代理返回给爬虫。
没有代理:
爬虫 ────────────────────────▶ 目标网站
(真实 IP 直接暴露,容易被封)
使用代理:
爬虫 ──▶ 代理服务器 ──▶ 目标网站
爬虫 ◀─ 代理服务器 ◀─ 目标网站
(目标网站只看到代理 IP)
为什么需要代理?
- 目标网站通常有 IP 频率限制:同一 IP 短时间内请求过多会被封禁或弹验证码。
- 使用代理后,请求分散到大量不同 IP 上,每个 IP 的请求频率都低于阈值。
- 封了一个代理 IP,换下一个即可,爬虫本体不受影响。
1.2 代理的分类
按协议分:
| 协议 | 适用场景 | 说明 |
|---|---|---|
| HTTP | 普通 HTTP 页面 | 只能代理 HTTP 请求 |
| HTTPS (CONNECT) | 加密网站(主流) | 通过 CONNECT 隧道转发 TLS 流量 |
| SOCKS4/5 | 通用 TCP/UDP | 更底层,可代理任意流量,Python 用 PySocks 库 |
按匿名度分(关键!):
| 匿名级别 | 目标网站看到的 | 适用性 |
|---|---|---|
| 透明代理 | 你的真实 IP + 代理 IP | ❌ 毫无用处,反而暴露自己 |
| 普通匿名 | 代理 IP,但知道你用了代理 | ⚠️ 可用,但容易被针对 |
| 高匿名(Elite) | 只看到代理 IP,不知道你用代理 | ✅ 爬虫首选 |
判断方法:向检测网站(如 httpbin.org/headers)发请求,观察返回的 X-Forwarded-For / Via 头:
- 有 X-Forwarded-For: 你的真实IP → 透明代理
- 有 Via / Proxy-Connection 但无真实 IP → 普通匿名
- 什么都没有 → 高匿名
1.3 免费 vs 付费代理
| 维度 | 免费代理 | 付费代理 |
|---|---|---|
| 价格 | 0 元 | 几十~几百元/天 |
| 可用率 | < 10%,随抓随失效 | > 90% |
| 稳定性 | 极差,几分钟内可能失效 | 有 SLA 保障 |
| 匿名度 | 参差不齐 | 可指定高匿名 |
| 适用场景 | 学习、练手 | 生产爬虫 |
免费代理来源:快代理、西刺(已停)、89IP、FreeProxyList 等网站的免费页面。免费代理的本质是"别人暴露在公网上的代理服务",随时可能关闭,所以免费代理必须配合验证机制才有价值。
二、原理解释
2.1 HTTP 代理的工作原理
HTTP 请求代理:
爬虫向代理发送的请求行直接就是完整目标 URL:
GET http://example.com/page HTTP/1.1 ← 注意:完整 URL
Host: example.com
代理看到完整 URL,替你向目标发起请求。
HTTPS 请求代理(CONNECT 隧道):
爬虫 ──▶ 代理: CONNECT example.com:443 HTTP/1.1
代理 ──▶ 爬虫: HTTP/1.1 200 Connection Established
(此后 TCP 连接变成透明隧道,爬虫在隧道内完成 TLS 握手)
爬虫 ◀═════════════ 加密数据 ═════════════▶ 目标:443
代理无法解密 HTTPS 内容,只能看到你在连哪个域名(SNI)。这就是为什么 requests 的 proxies 参数同时支持 http/https URL 走 HTTP 代理。
2.2 代理池的核心架构
代理池不是一个"池子数据结构",而是一个常驻服务,内部有四个逻辑模块:
flowchart LR
A[抓取器 Fetcher<br>从免费网站抓代理] --> B[验证器 Validator<br>并发检测可用性]
B -->|可用| C[存储器 Redis/内存<br>按分数排序]
B -->|不可用| C
C --> D[API 接口<br>随机返回高分数代理]
D --> E[爬虫业务]
C -.定时调度.-> A
C -.定时降分.-> B
四个模块职责:
- 抓取器(Fetcher):从多个免费代理网站爬取代理列表。来源越多越好,因为单来源可用率极低。
- 验证器(Validator):并发地对每个代理发起真实请求(访问可靠、响应快的检测站点),测量延迟、验证匿名度。验证失败的代理扣分,多次失败则移除。
- 存储器(Store):通常用 Redis 的 ZSet(有序集合),score 表示代理的"健康度"。
- API 接口(Provider):爬虫通过
GET /random获取当前最优代理,或GET /all获取全量列表。
分数机制(为什么用它?):
- 新抓的代理初始分 50(半信半疑)
- 每次验证成功 +10,封顶 100 → 进入"可用"队列
- 每次验证失败 -10,降到 0 → 移除
- 这样能容忍偶发抖动(一次超时不至于立刻淘汰),又能及时清理持续失效的代理
2.3 并发验证为什么用协程而不是线程
验证 1000 个代理 = 1000 次网络 IO(每次 1~5 秒)。
- 串行:1000 × 3s ≈ 50 分钟,不可接受
- 线程池(100):可行,但线程开销大、上限低
- 协程(asyncio + aiohttp):单线程内上千并发连接,IO 等待时切换任务,验证 1000 个代理只需 10~30 秒 ✅
代理验证是典型的 IO 密集型 + 高并发短连接场景,是协程的最佳用武之地。
三、定义与使用方法(API 速查)
3.1 requests 使用代理
import requests
proxies = {
"http": "http://112.34.5.6:8080",
"https": "http://112.34.5.6:8080", # https 流量也可走 http 代理(CONNECT 隧道)
}
# 带认证的代理
proxies_auth = {
"http": "http://user:pass@112.34.5.6:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
3.2 aiohttp 使用代理
import aiohttp, asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get(
"https://httpbin.org/ip",
proxy="http://112.34.5.6:8080",
) as resp:
print(await resp.json())
asyncio.run(main())
3.3 Scrapy 使用代理(中间件方式)
# middlewares.py
class ProxyMiddleware:
def process_request(self, request, spider):
request.meta["proxy"] = "http://112.34.5.6:8080"
3.4 常用方法参数速查
| 场景 | 方法/参数 | 说明 |
|---|---|---|
| requests 代理 | proxies={"http": ..., "https": ...} |
字典,key 是协议 |
| requests 超时 | timeout=(3, 5) |
(连接超时, 读取超时),验证代理必设 |
| aiohttp 代理 | session.get(url, proxy=...) |
直接传 proxy 参数 |
| aiohttp 超时 | aiohttp.ClientTimeout(total=8) |
总超时 |
| 验证失败重试 | tenacity / 手写 for 循环 |
代理失效是常态,业务代码必须重试换代理 |
| 环境变量代理 | HTTP_PROXY / HTTPS_PROXY |
requests 默认读取,注意本地调试时的干扰 |
⚠️ 避坑:
1. timeout 不设或设太大 → 验证阶段会被死代理拖死
2. 免费代理大多只支持 HTTP,访问 HTTPS 站点时失败不一定是代理问题,先确认协议
3. requests 的 proxies 字典 key 是 你访问的目标协议,不是代理本身的协议
四、图解
4.1 代理池生命周期总览
┌─────────────────────────────────────────────┐
│ 定时调度(每 10 分钟) │
└──────────────────┬──────────────────────────┘
▼
┌──────────┐ ┌────────────┐ ┌────────────────┐
│ Fetcher │────▶│ Validator │────▶│ Redis ZSet │
│ 抓免费代理 │ │ aiohttp并发 │ │ score: 0~100 │
└──────────┘ │ 逐个验证 │ └───────┬────────┘
└────────────┘ │
▲ ▼
│ ┌─────────────┐
失败扣分续验证 │ API Server │
│ │ GET /random │
┌──────┴──────┐ └──────┬──────┘
│ score=0 移除 │ ▼
└─────────────┘ 爬虫拿代理干活
4.2 分数流转状态机
抓取入库 验证成功 验证失败
┌───────┐ +10 ┌───────┐ -10 ┌───────┐
│ 50 │──────▶ │ 100 │◀────── │ 30 │
└───────┘ └───────┘ └───────┘
│ │
│ 连续失败分数归零 ▼
└──────────────▶ 0 ◀──────────── 删除代理
五、实战代码案例
完整的高匿名代理池实现见 code/03-proxy-pool.py,包含:
- 多来源免费代理抓取
- asyncio 并发验证(含匿名度检测)
- 内存 ZSet 式分数管理
- 随机取用 API + 业务侧自动换代理重试
核心使用方式:
pool = ProxyPool()
pool.fetch_all() # 抓取
await pool.validate_all() # 并发验证
proxy = pool.get_proxy() # 取最优代理
六、思考题
- 为什么免费代理的"可用率"这么低?从这些代理的来源(机房误配置、开放转发)角度分析。
- 验证代理时应该用哪个网站做检测目标?如果用你真正要爬的目标网站做验证,有什么好处和风险?
- 代理池的分数机制里,"一次失败扣 10 分而不是直接删除"的设计是为了解决什么问题?
- 如果目标网站不仅封 IP,还通过 TLS 指纹(JA3)识别并封锁 requests/curl 的流量,单纯换代理还有用吗?有什么思路?
- 如何评估"代理池越大越好"这个说法?代理数量与爬虫吞吐量之间的最优关系是什么?
参考阅读:
- requests proxies 文档:https://requests.readthedocs.io/en/latest/user/advanced/#proxies
- aiohttp proxy 支持:https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support