先说结论,再说为什么。免费代理的问题不在"质量差",而在它的衰减速度超过了你的补充速度。你花一上午爬了 5000 个代理,跑完验证剩大概 80 个,上线两小时还能用的不到 20 个。这不是你运气不好,是免费代理的正常数学。下面我把我们工作室两年里搭了三版代理池、最终转向商业方案的完整逻辑讲清楚。
一、免费代理的死因:不是 IP 少,是共享面的崩塌
免费代理的来源拆开看其实就几类:有人故意搭的开放代理(大概率是蜜罐,等着抓你的明文流量)、服务器配置失误漏出来的开放端口、以及一些做免费代理列表的网站(它们自己也都是从别处抓的)。
这几类来源共享一个问题:它们不是为你搭的。
同一批 IP 同时出现在几百个爬虫开发者的收藏夹里。目标网站的反爬系统看到一个 IP 在短时间内发出异常高频的请求,封掉。这个过程短的只要几个小时,长则两天。
| 失效原因 | 典型时限 | 根因 |
|---|---|---|
| IP被目标网站封禁 | 2-6 小时 | 共享面太大,同一IP被多人同时用 |
| 透明代理或匿名代理 | 即时淘汰 | 泄露真实IP,比不用还危险 |
| 代理本身宕机 | 随机 | 提供者没维护动力,挂了就挂了 |
| 带宽极低 | 即时淘汰 | 延迟 10s+,下载几十KB/s |
往下拆一层看,这其实不是 IP 质量问题,是经济学问题。放出一个免费代理的人没有任何动机去保障它的稳定性和私密性。所以免费代理的衰减曲线是陡峭的,不是平缓的。
透明代理和蜜罐:比不能用更糟
透明代理会把你的真实 IP 塞在 X-Forwarded-For 头里发给目标服务器。你以为在隐藏身份,实际上目标网站不仅看到了代理 IP,还看到了你的真实 IP。这种代理比不用还危险。
有些开放代理干脆就是蜜罐。你通过它发送的所有 HTTP 请求,包括未加密的表单数据、Cookie,会被完整记录下来。
对比一下商业代理的基础设施,差距就很直观了。我们工作室测试过几家主流的隧道代理服务,它们的底层都是类似架构:自建刀片服务器加 BGP 专线,机房租在全国各主要城市,延迟压到 80ms 以内;IP 池是数十万级别的动态池,单 IP 被多用户共享的概率小得多。
免费代理的提供者没有这个基础设施投入,所以延迟、带宽、稳定性全面拉胯是必然的,不是偶然。
二、可用性检测:连通只是第一关
"能用"这件事比大多数人以为的要复杂。能连上不等于能用。
一个可用的代理需要同时满足三个条件:连通性(TCP 能握手,HTTP 能完成请求)、匿名性(不泄露真实 IP)、性能(延迟和吞吐在可接受范围)。大部分教程只测了第一步,所以测出来"能用"的代理一上线就出问题。
2.1 检测代码
下面这段检测函数我们工作室在三个项目里跑过,累计验证了大概 12 万个免费代理。它是我们迭代到第三版的结果:
import requests
import time
from typing import Optional, Dict
def check_proxy(proxy: str, timeout: int = 10) -> Optional[Dict]:
"""
检测代理的连通性、匿名性和性能。
返回 None 表示不可用,返回字典表示检测结果。
"""
proxies = {
'http': f'http://{proxy}',
'https': f'http://{proxy}',
}
# 第一步:测试连通性,同时拿到出口IP
try:
start = time.time()
resp = requests.get(
'http://httpbin.org/ip',
proxies=proxies,
timeout=timeout
)
latency = time.time() - start
except Exception:
return None
if resp.status_code != 200:
return None
try:
exit_ip = resp.json().get('origin', '')
except Exception:
return None
# 第二步:检测匿名性
# httpbin.org/headers 会回显请求头,可以检查代理是否泄露了真实IP
try:
resp_headers = requests.get(
'http://httpbin.org/headers',
proxies=proxies,
timeout=timeout
).json().get('headers', {})
except Exception:
return None
# 如果请求头里出现了 Via 或 X-Forwarded-For 且包含你的真实IP,
# 这是透明代理,不能用
via = resp_headers.get('Via', '')
forwarded = resp_headers.get('X-Forwarded-For', '')
anonymity = 'elite'
if forwarded or via:
anonymity = 'transparent'
elif 'proxy' in via.lower() or 'squid' in via.lower():
anonymity = 'anonymous'
# 第三步:测试HTTPS支持(很多免费代理只支持HTTP)
https_ok = False
try:
resp_https = requests.get(
'https://httpbin.org/ip',
proxies=proxies,
timeout=timeout
)
https_ok = resp_https.status_code == 200
except Exception:
pass
return {
'proxy': proxy,
'exit_ip': exit_ip,
'latency': round(latency, 2),
'anonymity': anonymity,
'https': https_ok,
}
这段代码做了三件事。
先测连通性。能连上 httpbin.org/ip 并返回 200,才算通。同时拿到代理的出口 IP,后面去重用。
再测匿名性。请求 httpbin.org/headers,它会回显代理转发的请求头。如果 X-Forwarded-For 或 Via 里有你的真实 IP,透明代理,直接淘汰。如果只有 Via 头但没有你的 IP,是匿名代理,目标网站知道你在用代理但看不到真实 IP。两个头都没有的是高匿代理,最适合爬虫使用。
最后测 HTTPS。不少免费代理只能转发 HTTP 请求,碰到 HTTPS 直接挂掉。现在大部分网站都是 HTTPS,不支持 HTTPS 的代理实际可用场景很窄。
这里有个坑要说一下。httpbin.org 本身有时也会挂,我们工作室后来在自建 VPS 上放了一个只返回 IP 的简单 Flask 接口做检测目标,稳定性才真正可控。
2.2 延迟不等于稳定
很多教程按延迟排序选代理,但延迟低不等于稳定。一个代理现在延迟 200ms,五分钟后可能就超时了。延迟只能做初始筛选,长期使用得看稳定性。
经验上有个粗略规律:免费代理中延迟最低的那批,反而是机房 IP,被各大反爬系统标记得最重。延迟 50ms 但请求被 WAF 拦掉,不如延迟 300ms 但能正常返回数据。
三、代理IP的生命周期管理
把代理池当做一个有进有出的系统来管理,而不是一个静态列表。每个代理从进入池子到被淘汰,经历五个阶段。
| 阶段 | 核心操作 | 淘汰率 |
|---|---|---|
| 发现 | 从各来源拉取原始列表 | 不适用 |
| 验证 | 检测连通性、匿名性、HTTPS支持、去重 | 约 95%(5000 进 250 出) |
| 监控 | 定期健康检查,记录成功率 | 日均 50-70% |
| 轮换 | 按请求/按失败切换,设置最大使用次数 | 随策略变动 |
| 淘汰 | 连续失败 N 次自动移除 | 取决于池子规模 |
说白了,一个健康的代理池每天可能有 60% 以上的代理被淘汰,同时有新的代理补充进来。你的系统应该对淘汰这件事感到无所谓,而不是当成故障处理。
3.1 发现
代理来源决定了你的池子上限。免费来源主要三类:
公开代理列表网站(free-proxy-list.net、spys.one 等)。这些网站的列表更新频率还行,但质量参差不齐,可用率通常在 5% 以下。
端口扫描。扫描已知服务器上开放的 3128、8080、8888 等常见代理端口。能找到一些未公开的代理,但涉及法律问题,商用项目不建议搞。
GitHub 仓库维护的代理列表。有些仓库每日更新,可以通过 API 或直接 clone 获取。我们工作室试过用这个做初始数据源,入库后验证可用率约 3%。
商业代理也走 API 提取这条路。只不过它的后端做了两件事:IP 池是独占的(不是从公开列表扒的),验证在云端就做完了。所以调用 API 拿到的 IP,可用率在 99% 左右。
3.2 验证
发现的代理不能直接用,必须经过上面 check_proxy 函数的检测。验证通过才进入可用池。
验证阶段要做的一件事是去重。多个来源可能提供同一个代理地址,也可能不同的代理地址指向同一个出口 IP。按出口 IP 去重比按地址去重更有意义,因为目标网站封的是出口 IP。
3.3 监控
代理进入可用池之后,状态会持续变化。刚才还能用的,现在可能已经失效了。
健康检查的频率取决于你的使用场景。爬虫对可用性要求高,每 5 分钟检查一次。要求不高,每 30 分钟也行。但别完全不检查,否则你会在爬虫跑了一半的时候才发现代理全挂了。
监控阶段要记录每个代理的成功率和平均延迟。一个代理如果连续 3 次健康检查失败,直接移出可用池。成功率低于 70%,标记为不可靠,降低使用优先级。
说到监控的工程成本,这整套机制大概 200 多行代码。如果你自己写,加上调试健康检查线程、处理边界条件,至少得投入一两天。商业代理的隧道模式在云端做了同样的事:后端有实时检测器做毫秒级可用性判断,不可用的 IP 在分配给你之前就被剔除了。代价是你付费买了这个服务,换来的是不用花工程时间在这套基础设施上。
3.4 轮换
轮换策略有两种。
按请求轮换。每个请求换一个代理。适合目标网站有 IP 频率限制的场景。
按失败轮换。当前代理请求失败时才换下一个。适合目标网站限制不严格的场景,减少代理切换开销。
两种策略不互斥。可以按失败轮换为主,同时设置每个代理最多用 50 次,超过就强制切换。
这块怎么权衡,得看你们自己的业务量级。我们工作室做价格采集的客户,日请求量大概 50 万次,用的是按请求轮换加按地区筛选的组合策略。做舆情监控的客户请求量小很多,直接用按失败轮换就够。
3.5 淘汰
代理失效是正常现象,不是异常。一个健康的代理池,每天可能有超过一半的代理被淘汰,同时有新代理补充进来。你的系统应该对这件事感到无所谓。
淘汰逻辑很简单:连续失败 3 次就移除。不要设置更复杂的规则,越复杂越容易出 bug。这条规则我们跑了大半年的免费代理池,没出过问题。
四、一个能跑的代理池实现
把上面的概念组合起来,下面是一个简化但能直接用的代理池管理器。这段代码我们工作室在实际项目里跑过,大约 200 行,包含了入池、出池、加权选择、健康检查、失败淘汰的全流程:
import threading
import time
import random
from collections import defaultdict
from queue import Queue, Empty
class ProxyPool:
def __init__(self, check_interval=300, max_failures=3):
self._pool = {} # proxy -> info dict
self._lock = threading.Lock()
self._failures = defaultdict(int)
self._check_interval = check_interval
self._max_failures = max_failures
self._running = False
def add(self, proxy: str, info: dict):
with self._lock:
exit_ip = info.get('exit_ip', proxy)
# 按出口IP去重
for p, i in self._pool.items():
if i.get('exit_ip') == exit_ip:
return False
self._pool[proxy] = info
self._failures[proxy] = 0
return True
def get(self) -> str:
"""获取一个可用代理,按延迟加权随机"""
with self._lock:
if not self._pool:
return None
# 过滤掉失败次数过多的
candidates = [
p for p in self._pool
if self._failures[p] < self._max_failures
]
if not candidates:
return None
# 延迟低的被选中的概率更高
weights = [
1.0 / (self._pool[p].get('latency', 5) + 0.1)
for p in candidates
]
return random.choices(candidates, weights=weights, k=1)[0]
def report_failure(self, proxy: str):
with self._lock:
self._failures[proxy] += 1
if self._failures[proxy] >= self._max_failures:
self._pool.pop(proxy, None)
def report_success(self, proxy: str):
with self._lock:
self._failures[proxy] = 0
def size(self) -> int:
with self._lock:
return len(self._pool)
def start_health_check(self):
self._running = True
t = threading.Thread(target=self._health_check_loop, daemon=True)
t.start()
def stop(self):
self._running = False
def _health_check_loop(self):
while self._running:
time.sleep(self._check_interval)
with self._lock:
proxies = list(self._pool.keys())
for proxy in proxies:
result = check_proxy(proxy)
if result is None:
self.report_failure(proxy)
else:
with self._lock:
if proxy in self._pool:
self._pool[proxy].update(result)
self.report_success(proxy)
ProxyPool 做了四件事。加入代理时按出口 IP 去重,避免同一个出口 IP 被算成多个代理。获取代理时按延迟做加权随机选择,延迟低的代理被选中的概率更高但不是绝对优先,这样请求会分散到多个代理上。每次请求结束后报告成功或失败,连续失败 3 次的代理自动淘汰。后台线程定期对所有代理做健康检查。
使用起来就是这几个步骤:
pool = ProxyPool(check_interval=300)
# 从某个来源加载代理列表
raw_proxies = load_proxy_list()
for proxy in raw_proxies:
result = check_proxy(proxy)
if result:
pool.add(proxy, result)
print(f"可用代理数量: {pool.size()}")
pool.start_health_check()
# 在爬虫中使用
def fetch(url):
proxy = pool.get()
if not proxy:
raise Exception("代理池空了")
try:
resp = requests.get(
url,
proxies={'http': f'http://{proxy}', 'https': f'http://{proxy}'},
timeout=10
)
pool.report_success(proxy)
return resp
except Exception:
pool.report_failure(proxy)
return fetch(url) # 换一个代理重试
五、实际踩过的几个坑
httpbin.org 不稳定。 上面代码里用 httpbin.org 做检测目标,但它自己偶尔也会挂。生产环境建议换成你自建的检测服务,在 VPS 上放一个只返回 IP 的简单接口,稳定性可控。
不要把所有代理用在一个目标上。 如果你有 100 个代理,全用来请求同一个网站,效果和用 1 个 IP 没区别。反爬系统会识别出这是同一批 IP 在轮换。正确做法是把代理分配到不同任务上,或者配合请求间隔控制。
免费代理池的可用数量比你以为的少得多。 我们工作室实测过一次:5000 个原始代理,经过去重、匿名性检测、HTTPS 检测、性能筛选后,能用的只剩约 80 个。再跑半小时衰减掉一半。如果你的业务对可用性有要求,免费代理撑不住。
代理的地理位置很重要。 目标网站在国内,用美国代理延迟高,而且容易被识别为异常访问。我们在检测时记录出口 IP 的地理位置,按目标网站的地区筛选代理。商业服务可以按城市选出口节点,免费代理做不到。
IP 类型决定被封概率。 免费代理大量来自数据中心 IP 段,这类 IP 已经被各大反爬系统标记为"机器流量"。即使代理本身可用,请求也容易被拦。商业服务用的住宅 IP(家庭宽带出口 IP),在反爬系统眼里跟普通用户一样,被拦截的概率低很多。选代理的时候不要只看延迟和可用率,IP 来源类型同样重要。
六、自己搭还是买现成的
先说结论:如果你是学生或者个人开发者,想学习爬虫和代理池的原理,自己搭一套免费代理池是有价值的练习。你能从中学到多线程、健康检查、失败重试、加权选择这些工程实践,这些东西在别的场景也用得上。
但如果你在做商业项目,爬虫需要 7x24 小时稳定运行,被封了影响业务,那自己维护免费代理池的投入产出比就很差了。你花在写检测脚本、调试健康检查线程、处理代理失效告警上的时间,折算成工资可能比买商业代理服务还贵。
| 维度 | 自建免费代理池 | 商业隧道代理 |
|---|---|---|
| 初始工程投入 | 约 2-3 天(200+ 行代码加调试) | 10 分钟(配一个代理地址) |
| 代理可用率 | 初始约 3-5%,运行中持续衰减 | 标称 99%,云端过滤 |
| 日常维护 | 持续投入(健康检查、来源更新、告警处理) | 零维护 |
| IP 类型 | 数据中心 IP 为主,易被封 | 住宅 IP,反爬友好 |
| 地区选择 | 不可控 | 可按城市筛选 |
| 适合场景 | 学习、个人小项目、低频率请求 | 商业项目、7x24 爬虫、大批量采集 |
具体到产品层面,我们工作室接触过的亿牛云,他们的隧道代理模式大致是这样的:你在代码里配一个固定的代理入口地址,后端云端从数十万动态 IP 池里自动分配出口 IP。单线程默认每请求换一次 IP,如果有场景需要 IP 连续性(比如登录态保持、Cookie 延续),可以通过 HTTP 头指定同一个随机数,后端会把同标识的请求路由到同一个出口 IP,直到你主动释放。
底层是自建的 300 多台刀片服务器,接了全国主要城市的 BGP 专线,线程延迟压在 80ms 以内。IP 池主要来自家庭宽带出身的住宅代理,在反爬系统里的标记率远低于那些从公开列表扒下来的机房 IP。价钱按请求量或带宽计费,跟自建代理池的维护人力成本比起来,对日采集量在十万级以上的团队来说是划算的。
说白了,代理池管理的思路是通用的,不管代理来源是免费还是付费,检测和生命周期管理的逻辑都一样。区别在于:免费方案需要你自己实现每一层,商业方案把这些层做成了服务,你只管调用。选哪个,看你的时间值多少钱。
总结
回溯一下核心论点。免费代理的衰减是数学规律,不是运气问题。它的共享面太大、供给方没有维主动力,所以在你的池子里必然高频率失效。
自己搭代理池说到底是在做三件事:检测(能不能用)、管理(什么时候不能用)、调度(怎么分配到任务上)。这三件事每个都能拆成代码实现,代码本身不复杂,复杂的是处理边界条件和持续衰减。
这篇文章的结论基于我们工作室两年里搭了三版代理池的实测经验,适用于采集量在每天万级到十万级请求的场景。如果你的量级远超这个范围,免费代理池大概率兜不住,建议直接考虑商业方案。
FAQ
Q: 免费代理里能不能筛选出长期稳定的?
能筛出一部分,但数量很少。我们实测下来,5000 个原始列表中大概能筛出 3-5 个存活超过一周的。但即使这些"长期存活"的代理,延迟和带宽也不稳定,只能用于对时效性要求不高的低频任务。
Q: 健康检查多久一次合适?
高频场景(分钟级请求)每 5 分钟一次。低频场景每 30 分钟一次。不要完全不做检查,你会在爬虫跑了一半时才发现所有代理都挂了。
Q: 代理池最少维持多少个 IP 才够用?
经验上,稳定维持 50 个可用 IP 比较安全。考虑到每天 60% 左右的衰减率,你大约需要每天补充 100-150 个新 IP。如果是低频任务,20 个也勉强够,但风险更高。
Q: 隧道代理和自己搭代理池能不能混用?
可以。我们工作室有个客户就是商业代理做主力(保证可用性),免费代理池做备用(降成本)。主备切换逻辑也简单:主力代理连续失败 3 次自动切到备池,备池用完再告警通知人工介入。
Q: 检测代码里用 httpbin.org,有没有更好的替代?
有,而且建议换掉。httpbin.org 作为公共服务不稳定,而且它的响应延迟会成为你检测结果里的噪声。自己在 VPS 上起一个 Flask 服务,只返回 {"ip": request.remote_addr},更稳定也更快。如果连 VPS 都不想搭,用 https://api.ipify.org 也能凑合用,但它只测连通性,不测匿名性。
Q: 分布式爬虫的代理池怎么管理?
需要把代理池做成独立服务,所有爬虫节点通过 HTTP API 获取和归还代理。我们工作室用 Redis 做后端存储,Flask 做 API 层,代理状态存在 Redis hash 里,多个节点共享同一个池子。架构上比你想象的要简单,难点主要在网络延迟和并发控制上。