我用动态调度算法把代理成本降了90%

0 阅读11分钟

写在前面

上一篇文章《Python爬虫请求头伪装后仍被反爬,基于代理池+随机延迟的绕过实战》里,我提到过一个核心结论:IP信誉评分占到了反爬拦截决策权重的60%以上。文章发出后,后台收到不少留言,问得最多的就是——“代理池到底怎么搭?”

说实话,市面上免费代理一大堆,但真正能用的不到10%。付费代理虽然稳定,但单靠一家服务商的IP池,碰上大规模采集任务,照样捉襟见肘。

所以这篇文章,我打算手把手带你从零搭建一个属于自己的代理池。多源接入、自动验证、智能调度、全自动运行——这些功能一个不落,代码全部可运行,拿到就能用。


一、为什么需要自己搭代理池

先泼一盆冷水:网上那些“一键获取免费代理”的教程,十个有九个是坑。

免费代理的问题在于:

  • 可用率极低:从公开代理站爬下来的IP,能用的往往不到5%,剩下的要么连不上,要么延迟高到怀疑人生

  • 已被标记:大量爬虫共用同一批免费代理,目标网站的IP信誉系统早就把这些IP拉黑了

  • 随时失效:免费代理的生命周期通常以分钟计,你的爬虫刚跑起来,IP就挂了

付费代理能解决可用率的问题,但单一服务商的IP池也有天花板——比如某代理站只提供2000个IP,你的任务需要采集50万条数据,每个IP即使只发50次请求,也需要1万个不同的IP,这中间就差了5倍的缺口。

所以一个靠谱的代理池,核心能力就两条:

  1. 多源汇聚:同时接入免费源和付费源,把IP池的深度和广度都拉满

  2. 自动汰换:实时验证、自动剔除失效IP,保证池子里始终是可用的“活水”


二、代理池整体架构

先看一张架构图,心里有个全景:

三个模块各司其职:

| 模块 | 职责 | 核心逻辑 | | --- | --- | --- | | 代理获取器 | 从多个源拉取代理IP | 定时任务 + 多源并发拉取 + 去重 | | 代理验证器 | 检测代理是否可用 | 连通性测试 + 目标站实测 + 匿名性分级 | | 代理调度器 | 分配最优代理给爬虫 | 加权轮询 + 使用计数 + 冷却与淘汰 |

下面我们一个模块一个模块来写。


三、模块一:代理获取器(ProxyFetcher)

3.1 设计思路

获取器要做的事情很简单:定时从多个来源拉取代理IP,去重后存入池中

这里的关键是“多源”。单一来源的IP池深度有限,而且一旦服务商出问题,整个爬虫就瘫痪了。多源接入相当于给代理池上了“多重保险”。

3.2 免费代理源

虽然是免费代理,但聊胜于无。我选了几个相对稳定的免费源:

import requests
import re
import time
from typing import List, Dict

FREE_PROXY_SOURCES = [
{
"name": "proxy-list",
"url": "https://www.proxy-list.download/api/v1/get?type=http",
"parser": "text" # 返回纯文本,每行一个 ip:port
},
{
"name": "proxylistplus",
"url": "https://list.proxylistplus.com/Fresh-HTTP-Proxy-List-1",
"parser": "html" # 需要从HTML中解析
},
]


def fetch_from_free_sources() -> List[Dict[str, str]]:
"""从免费源拉取代理,返回去重后的代理列表"""
proxies = []
seen = set()

for source in FREE_PROXY_SOURCES:
try:
resp = requests.get(source["url"], timeout=10)
if resp.status_code != 200:
continue

# 简单正则提取 ip:port
pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{2,5})'
matches = re.findall(pattern, resp.text)

for ip, port in matches:
key = f"{ip}:{port}"
if key not in seen:
seen.add(key)
proxies.append({"ip": ip, "port": int(port), "source": source["name"]})
except Exception:
continue

return proxies

⚠️ 坦诚提醒:免费代理的可用率确实很低,上面的代码主要是为了演示“多源汇聚”的思路。实际生产环境中,建议把重心放在付费代理源上。

3.3 付费代理源接入

付费代理服务商通常提供API接口,调用后直接返回可用IP列表。这里我封装一个通用的付费源接入器:

PAID_PROXY_SOURCES = [
{
"name": "provider_a",
"url": "https://api.provider-a.com/get?num=20&type=http",
"api_key": "your_api_key_here", # 替换为你的API Key
"headers": {"Authorization": "Bearer your_token"}
},
# 可以继续添加更多服务商
]


def fetch_from_paid_sources() -> List[Dict[str, str]]:
"""从付费源拉取代理"""
proxies = []
seen = set()

for source in PAID_PROXY_SOURCES:
try:
resp = requests.get(
source["url"],
headers=source.get("headers", {}),
timeout=15
)
if resp.status_code != 200:
continue

data = resp.json()
# 假设返回格式为 {"data": [{"ip": "...", "port": ...}, ...]}
for item in data.get("data", []):
key = f"{item['ip']}:{item['port']}"
if key not in seen:
seen.add(key)
proxies.append({
"ip": item["ip"],
"port": item["port"],
"source": source["name"]
})
except Exception:
continue

return proxies

3.4 定时拉取与自动补充

获取器需要定时运行,持续为代理池补充新鲜血液。我用一个简单的循环来实现:

import threading

class ProxyFetcher:
def __init__(self, pool, interval: int = 300):
"""
pool: 代理池实例
interval: 拉取间隔(秒),默认5分钟
"""
self.pool = pool
self.interval = interval
self._running = False

def start(self):
"""启动定时拉取任务"""
self._running = True
thread = threading.Thread(target=self._run, daemon=True)
thread.start()

def stop(self):
self._running = False

def _run(self):
while self._running:
# 拉取免费源
free_proxies = fetch_from_free_sources()
self.pool.add_proxies_batch(free_proxies)

# 拉取付费源
paid_proxies = fetch_from_paid_sources()
self.pool.add_proxies_batch(paid_proxies)

# 等待下一次拉取
time.sleep(self.interval)

四、模块二:代理验证器(ProxyValidator)

4.1 为什么要验证

代理池里塞了一堆IP,不代表这些IP都能用。验证器要回答三个问题:

  1. 能连通吗? —— 基础的TCP连接测试

  2. 能访问目标站吗? —— 拿目标URL做一次实际请求

  3. 是匿名代理吗? —— 目标站看到的IP是代理IP还是你的真实IP

4.2 验证器完整实现

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import List, Dict, Optional

class ProxyValidator:
def __init__(self, test_url: str = "https://httpbin.org/ip", timeout: int = 8):
"""
test_url: 用于验证代理可用性的测试地址
timeout: 单个代理的验证超时时间
"""
self.test_url = test_url
self.timeout = timeout

def validate_single(self, proxy: Dict[str, str]) -> Optional[Dict]:
"""
验证单个代理,返回带评分的代理信息,不可用则返回None
"""
proxy_url = f"http://{proxy['ip']}:{proxy['port']}"
proxies = {"http": proxy_url, "https": proxy_url}

score = 0
try:
start = time.time()
resp = requests.get(self.test_url, proxies=proxies, timeout=self.timeout)
elapsed = time.time() - start

if resp.status_code == 200:
score += 1 # 连通性通过

# 检查返回的IP是否是代理IP(而非真实IP)
origin_ip = resp.json().get("origin", "")
if proxy["ip"] in origin_ip:
score += 1 # 匿名性通过

# 响应速度评分:越快越好
if elapsed < 2:
score += 2
elif elapsed < 5:
score += 1

return {
"ip": proxy["ip"],
"port": proxy["port"],
"source": proxy.get("source", "unknown"),
"score": score,
"latency": round(elapsed, 2),
"validated_at": time.time()
}
except Exception:
pass

return None

def validate_batch(self, proxies: List[Dict], max_workers: int = 20) -> List[Dict]:
"""
并发验证一批代理,返回验证通过的代理列表
"""
valid_proxies = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(self.validate_single, p): p for p in proxies}
for future in as_completed(futures):
result = future.result()
if result:
valid_proxies.append(result)

return valid_proxies

评分规则说明

  • 基础连通:+1分

  • 匿名性验证通过:+1分

  • 响应时间 < 2秒:+2分

  • 响应时间 2-5秒:+1分

  • 满分4分,代表“高速匿名代理”


五、模块三:代理调度器(ProxyDispatcher)

5.1 调度策略

调度器是代理池的“大脑”,负责决定“下一个请求用哪个代理”。我设计了三个核心策略:

策略一:分数优先

验证器给每个代理打了分,调度时优先分配高分代理,保证请求质量。

策略二:使用均衡

同一个IP不能一直用,需要通过使用计数来均衡负载,避免单个IP被过度使用。

策略三:冷却机制

代理被使用后需要冷却一段时间,模拟真实用户的访问间隔。冷却时间根据代理的评分动态调整——高分代理冷却短,低分代理冷却长。

5.2 调度器完整实现

import heapq
from dataclasses import dataclass, field
from typing import Optional, Dict, List

@dataclass(order=True)
class ProxyNode:
"""代理节点,支持优先队列排序"""
priority: int # 优先级 = 分数 - 使用次数 * 惩罚系数
ip: str = field(compare=False)
port: int = field(compare=False)
score: int = field(compare=False)
total_requests: int = field(compare=False)
last_used: float = field(compare=False)
source: str = field(compare=False)


class ProxyDispatcher:
def __init__(self, cooldown_base: int = 30, max_use_per_ip: int = 50):
"""
cooldown_base: 基础冷却时间(秒)
max_use_per_ip: 单个IP最大使用次数,超过后自动淘汰
"""
self.cooldown_base = cooldown_base
self.max_use_per_ip = max_use_per_ip
self._heap: List[ProxyNode] = []
self._all_proxies: Dict[str, ProxyNode] = {}
self._lock = threading.Lock()

def add_proxy(self, proxy_info: Dict):
"""添加新代理到调度器"""
key = f"{proxy_info['ip']}:{proxy_info['port']}"
with self._lock:
if key in self._all_proxies:
return # 已存在,跳过

node = ProxyNode(
priority=0,
ip=proxy_info["ip"],
port=proxy_info["port"],
score=proxy_info.get("score", 1),
total_requests=0,
last_used=0,
source=proxy_info.get("source", "unknown"),
)
self._all_proxies[key] = node
heapq.heappush(self._heap, node)

def get_proxy(self) -> Optional[Dict[str, str]]:
"""获取一个最优可用代理"""
with self._lock:
now = time.time()

while self._heap:
node = heapq.heappop(self._heap)

# 检查是否超过最大使用次数
if node.total_requests >= self.max_use_per_ip:
del self._all_proxies[f"{node.ip}:{node.port}"]
continue

# 检查冷却时间
cooldown = self.cooldown_base / max(node.score, 1)
if now - node.last_used < cooldown:
# 还在冷却中,放回去
heapq.heappush(self._heap, node)
return None # 没有可用代理

# 更新状态
node.total_requests += 1
node.last_used = now
node.priority = node.total_requests # 使用次数越多,优先级越低

heapq.heappush(self._heap, node)

return {
"http": f"http://{node.ip}:{node.port}",
"https": f"http://{node.ip}:{node.port}",
}

return None

def report_failure(self, ip: str, port: str):
"""报告代理失效,从调度器中移除"""
key = f"{ip}:{port}"
with self._lock:
if key in self._all_proxies:
del self._all_proxies[key]
# 重建堆(简化处理,生产环境可优化)
self._heap = [n for n in self._heap if f"{n.ip}:{n.port}" != key]
heapq.heapify(self._heap)

def get_stats(self) -> Dict:
"""获取当前代理池统计信息"""
with self._lock:
total = len(self._all_proxies)
available = sum(1 for n in self._all_proxies.values()
if n.total_requests < self.max_use_per_ip)
return {
"total": total,
"available": available,
"max_use_per_ip": self.max_use_per_ip,
}

六、完整整合:代理池主类

把三个模块整合到一起,封装成一个完整的代理池类:

import threading
import time
import logging
from typing import List, Dict, Optional

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


class ProxyPool:
"""
完整代理池,整合获取、验证、调度三大模块
"""

def __init__(self,
test_url: str = "https://httpbin.org/ip",
pool_min_size: int = 20,
fetch_interval: int = 300,
validate_interval: int = 180):
"""
test_url: 代理验证测试地址
pool_min_size: 代理池最小容量,低于此值自动补充
fetch_interval: 代理拉取间隔(秒)
validate_interval: 代理验证间隔(秒)
"""
self.dispatcher = ProxyDispatcher()
self.validator = ProxyValidator(test_url=test_url)
self.pool_min_size = pool_min_size
self.fetch_interval = fetch_interval
self.validate_interval = validate_interval

self._pending_proxies: List[Dict] = [] # 待验证的代理队列
self._lock = threading.Lock()
self._running = False

def add_proxies_batch(self, proxies: List[Dict]):
"""批量添加代理到待验证队列"""
with self._lock:
self._pending_proxies.extend(proxies)
logger.info(f"已添加 {len(proxies)} 个代理到待验证队列")

def _validate_loop(self):
"""验证循环:定时验证待验证队列中的代理"""
while self._running:
with self._lock:
if self._pending_proxies:
batch = self._pending_proxies[:100] # 每次最多验证100个
self._pending_proxies = self._pending_proxies[100:]
else:
batch = []

if batch:
valid = self.validator.validate_batch(batch)
for proxy in valid:
self.dispatcher.add_proxy(proxy)
logger.info(f"验证完成:{len(batch)} 个代理中 {len(valid)} 个可用")

time.sleep(self.validate_interval)

def _fetch_loop(self):
"""拉取循环:定时从各源拉取新代理"""
while self._running:
stats = self.dispatcher.get_stats()
if stats["available"] < self.pool_min_size:
logger.info(f"代理池可用数量不足 ({stats['available']}/{self.pool_min_size}),开始拉取...")

free_proxies = fetch_from_free_sources()
self.add_proxies_batch(free_proxies)

paid_proxies = fetch_from_paid_sources()
self.add_proxies_batch(paid_proxies)

time.sleep(self.fetch_interval)

def start(self):
"""启动代理池服务"""
self._running = True

# 启动验证线程
validate_thread = threading.Thread(target=self._validate_loop, daemon=True)
validate_thread.start()

# 启动拉取线程
fetch_thread = threading.Thread(target=self._fetch_loop, daemon=True)
fetch_thread.start()

logger.info("代理池已启动")

def stop(self):
"""停止代理池服务"""
self._running = False
logger.info("代理池已停止")

def get_proxy(self) -> Optional[Dict[str, str]]:
"""获取一个可用代理"""
return self.dispatcher.get_proxy()

def report_failure(self, ip: str, port: str):
"""报告代理失效"""
self.dispatcher.report_failure(ip, port)

def get_stats(self) -> Dict:
"""获取代理池统计信息"""
return self.dispatcher.get_stats()

七、实战测试

7.1 启动代理池

# 创建代理池实例
pool = ProxyPool(
test_url="https://httpbin.org/ip",
pool_min_size=20,
fetch_interval=300, # 5分钟拉取一次
validate_interval=180 # 3分钟验证一次
)

# 启动代理池
pool.start()

# 等待一段时间,让代理池积累足够的可用IP
time.sleep(30)

7.2 在爬虫中使用代理池

import requests
import random

def crawl_with_proxy_pool(url: str, pool: ProxyPool, max_retries: int = 3):
"""使用代理池发起请求,失败自动重试并切换代理"""
for attempt in range(max_retries):
proxy = pool.get_proxy()
if not proxy:
logger.warning("代理池已空,等待补充...")
time.sleep(10)
continue

try:
resp = requests.get(
url,
proxies=proxy,
timeout=10,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
)

if resp.status_code == 200:
return resp.text

if resp.status_code in [403, 429]:
# 代理被目标站封禁,报告失效
proxy_url = proxy["http"]
ip, port = proxy_url.replace("http://", "").split(":")
pool.report_failure(ip, port)
logger.warning(f"代理 {ip}:{port} 被封锁,已移除")

except Exception as e:
logger.warning(f"请求失败: {e},尝试下一个代理")

# 随机延迟,避免触发频率限制
time.sleep(random.uniform(1, 3))

return None


# 使用示例
html = crawl_with_proxy_pool("https://www.example.com/product/12345", pool)
if html:
print("数据获取成功!")
else:
print("所有代理均失败,请检查代理池状态")

7.3 查看代理池状态

stats = pool.get_stats()
print(f"代理池状态:总数 {stats['total']},可用 {stats['available']}")

八、总结与优化方向

8.1 你已经拥有了什么

一套完整的、可运行的代理池系统,包含:

| 能力 | 实现方式 | | --- | --- | | 多源代理拉取 | 免费源 + 付费源,定时自动拉取 | | 代理质量验证 | 连通性 + 匿名性 + 响应速度三重检测 | | 智能调度分配 | 分数优先 + 使用均衡 + 冷却机制 | | 自动汰换 | 失效代理自动剔除,超限代理自动淘汰 | | 全自动运行 | 拉取、验证、调度三线程协同,无需人工干预 |

8.2 可以进一步优化的方向

  • 持久化存储:把代理存到Redis或SQLite,重启后不丢失已验证的代理

  • 自适应冷却:根据目标站的返回状态码(403/429/200)动态调整冷却时间

  • Web管理界面:用Flask搭一个简易面板,可视化展示代理池状态

  • 代理质量衰减:长时间未使用的代理自动降分,模拟真实IP的“休眠”行为

  • 目标站专项验证:针对不同目标站分别验证,因为一个代理在A站可用不代表在B站也能用

8.3 最后想说

搭建代理池这件事,本质上是在和反爬系统打一场“信息不对称”的仗。你手里的IP越多、越分散、行为越像真人,对方就越难判断你到底是爬虫还是用户。

这个代理池,就是你在这场博弈里最扎实的底牌。


下一篇预告:如何用这个代理池,配合随机延迟和Cookie池,搭建一套企业级的分布式爬虫系统。