mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
4800 字
13 分钟
IP代理池的实现
2026-08-17

Python 爬虫代理实战 —— 从一行代码到生产级代理池 🕵️#

每个爬虫人的成人礼:兴冲冲跑通爬虫,第二天再跑,满屏 403。 本篇从 requests 加一行参数讲起,一路升级到能扛生产流量的代理池。由简到难,边讲边搓。

目录#

  1. 为什么需要代理:IP 被封的那一天
  2. 代理入门:requests 加一行参数
  3. 硬核原理:代理是怎么替你发请求的
  4. 代理从哪来:免费、付费、自建、隧道
  5. 代理体检:先验证再上岗
  6. 代理轮换:别老用一张脸敲门
  7. 手搓迷你代理池(单机版)
  8. 生产级代理池:架构与完整实现
  9. 踩坑指南与合规红线

1. 为什么需要代理:IP 被封的那一天#

1.1 你被封过 IP 吗?#

爬虫跑得正欢,突然所有请求返回 403 或者直接超时——恭喜,你的 IP 进了网站的风控名单。

网站反爬的第一道防线逻辑朴素得很:

同一个 IP,短时间请求太频繁 → 标记 → 限制 → 封禁

你家里的宽带 IP 是固定的(或至少半天内不变)。爬虫一秒钟几十个请求砸过去,在网站眼里就是”一个疯子敲门”,不封你封谁?

1.2 代理是什么#

代理(Proxy),说白了就是中间人

  • 你的请求不直接发给目标网站,而是先发给代理服务器
  • 代理服务器替你转发给目标网站,再把响应转回来
  • 目标网站看到的 IP 是代理的 IP,不是你的

换个更贴切的比喻:代购。你不出面,代购替你进店买东西,店家只认识代购,不认识你。被封的永远是代购,你换一个代购继续买。

1.3 爬虫用代理要解决的四件事#

需求说明
🎭 隐藏真实 IP保护自己,不暴露身份
🚦 突破频率限制多 IP 分摊请求量,一个 IP 每天敲 10 下而不是 10000 下
🌍 突破地域限制有些内容分地区展示,换地区 IP 能看到不同数据
🩹 应对封禁封了一个换一个,池子里永远有备胎

⚠️ 说在前面:代理是合法技术(企业用它做数据采集、测试、隐私保护),但用代理去爬违法违规数据、攻击网站、绕过付费墙就是另一回事了。第九节专门讲红线。


2. 代理入门:requests 加一行参数#

2.1 最简用法#

你手里有一个代理 1.2.3.4:8080(格式永远是 IP:端口),用起来只需要加一个 proxies 参数:

import requests
proxies = {
"http": "http://1.2.3.4:8080",
"https": "http://1.2.3.4:8080",
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print(resp.json()) # {"origin": "1.2.3.4"} ← 网站看到的你的 IP

跑通之后看输出:origin 显示的是 1.2.3.4(代理 IP),而不是你的真实 IP——代理生效了。

🔬 httpbin.org 是你的第一个测试神器/ip 会返回”它看到的你的 IP”;/headers 会返回请求携带的所有请求头。验证代理、检测匿名性全靠它。

2.2 带账号密码的代理#

付费代理大多要认证,格式是 http://用户名:密码@IP:端口

proxies = {
"http": "http://user123:pass456@1.2.3.4:8080",
"https": "http://user123:pass456@1.2.3.4:8080",
}

2.3 各库的代理写法速查#

写法备注
requestsrequests.get(url, proxies={"http": p, "https": p})最常用
urllibbuild_opener(ProxyHandler({"http": p}))标准库,写法啰嗦
httpxhttpx.Client(proxy=p)同步异步通吃
aiohttpsession.get(url, proxy=p)异步首选
# httpx 同步版
import httpx
with httpx.Client(proxy="http://1.2.3.4:8080", timeout=10) as client:
r = client.get("https://httpbin.org/ip")
print(r.json())
# aiohttp 异步版
import aiohttp, asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org/ip",
proxy="http://1.2.3.4:8080") as resp:
print(await resp.json())
asyncio.run(main())

2.4 为什么 httphttps 要分开写#

因为走代理时,HTTP 和 HTTPS 的工作方式完全不同(下一节详细讲原理):

  • http:// 开头的代理 → HTTP 请求走”明文转发”
  • https:// 开头的代理 → HTTPS 请求走”CONNECT 隧道”

所以一个代理可能支持 HTTP 转发、不支持 HTTPS 隧道。分开写,requests 才知道遇到哪种请求该找哪个代理。如果你只写一个,遇到另一种协议就会绕过代理直连——小心 IP 泄露!


3. 硬核原理:代理是怎么替你发请求的#

这节是全篇最硬核的部分,看懂它你就理解了代理的一切怪毛病。

3.1 HTTP 代理:明文转发#

访问 HTTP 网站时,请求本身是明文。代理收到请求后直接替你转发即可:

sequenceDiagram participant C as 爬虫程序 participant P as 代理服务器 participant S as 目标网站 C->>P: GET http://目标站/页面<br/>(注意:带完整URL) Note over P: 代理拆开URL<br/>知道该转发给谁 P->>S: GET /页面<br/>(只发路径,加上自己的IP) S-->>P: 200 响应 P-->>C: 200 响应 Note over S: 网站日志里只有代理的IP

注意细节:发给代理的请求行带完整 URLGET http://目标站/页面),而代理转发给目标站时只发路径(GET /页面)。这就是代理和”直连”在 HTTP 报文层面唯一的区别。

3.2 HTTPS 代理:CONNECT 隧道#

访问 HTTPS 网站时,内容全程加密。代理看不见、也不该看见内容。那它怎么转发?答案是先开一条”盲管道”:

sequenceDiagram participant C as 爬虫程序 participant P as 代理服务器 participant S as 目标网站 C->>P: CONNECT 目标站:443 P-->>C: 200 Connection Established Note over C,P: 隧道建立,代理从此只看<br/>"字节流"不看不改 C->>P: TLS握手+加密数据 P->>S: 原样转发(字节对字节) S-->>P: 加密数据 P-->>C: 原样转发

CONNECT 方法(RFC 7231)是 HTTPS 代理的核心:

  1. 客户端先发 CONNECT 目标站:443,意思是”给我开条到这里的管道”
  2. 代理连上目标站后回 200 Connection Established
  3. 之后代理变成纯搬运工——收到的每个字节原样转发,不解密、不修改

所以:HTTPS 代理看不到你的请求内容(能看到的是”你在什么时候连了哪个域名”)。那些声称”HTTPS 代理能窃取密码”的说法是错的——除非它伪造证书(中间人攻击),那浏览器会报安全警告。

3.3 透明 / 匿名 / 高匿:三种伪装等级#

这是代理质量的另一个维度——网站能从你身上挖出多少真实信息

级别会不会加特殊头网站看到的爬虫评价
🔍 透明代理X-Forwarded-For 附上你真 IP知道你用代理+你真 IP❌ 等于裸奔
🕶️ 匿名代理Via 头但藏真 IP知道你用代理,不知道你是谁⚠️ 部分网站会拦
🥷 高匿代理什么都不加以为代理 IP 就是真人爬虫只认这个

检测方法(用 httpbin 一秒看穿):

import requests
proxies = {"http": "http://1.2.3.4:8080", "https": "http://1.2.3.4:8080"}
r = requests.get("https://httpbin.org/headers", proxies=proxies, timeout=10)
headers = r.json()["headers"]
print("X-Forwarded-For" in headers) # True = 透明代理,真IP泄露!
print("Via" in headers) # True = 匿名代理,被标记为代理流量
# 两个都是 False = 高匿,放心用

3.4 SOCKS5:更高层的”通用管道”#

HTTP/HTTPS 代理只懂 HTTP 协议;SOCKS5 代理工作在 TCP 层,不管上层是 HTTP、FTP 还是别的,通通能转发。

  • socks5://:域名解析在你的电脑上做(可能 DNS 泄露)
  • socks5h://:域名解析在代理服务器上做(推荐,更安全)
# 需要 pip install requests[socks]
proxies = {
"http": "socks5h://user:pass@1.2.3.4:1080",
"https": "socks5h://user:pass@1.2.3.4:1080",
}
对比HTTP 代理SOCKS5
工作层应用层(懂 HTTP)传输层(只懂字节)
适用网页爬虫任何 TCP 协议
速度略快(协议简单)略慢(通用性代价)
认证Basic 认证用户名密码

🎯 爬虫选型结论:普通网页爬虫用 HTTP/HTTPS 代理就够;需要跑非 HTTP 协议(比如数据库直连、游戏接口)才上 SOCKS5。


4. 代理从哪来:免费、付费、自建、隧道#

4.1 免费代理:白嫖一时爽,维护火葬场#

公开代理列表站、GitHub 上每日更新的免费代理列表……看起来很香,实际:

缺点现实
延迟普遍 2~10 秒
短命平均存活几分钟到几小时
不安全不少是被人入侵的”肉鸡”,数据可能被截获
不稳定你验证完是活的,用的时候已经死了

定位:学习、测试、写教程。生产环境千万别用。

4.2 付费代理:生产的标配#

付费代理厂商(芝麻、快代理、青果、品易等,不背书,自己比较)按不同计费模式卖:

模式说明适合
按条数一包 N 个 IP,用多久随你小规模
按时长一个 IP 租 N 分钟需要稳定会话
按流量隧道按 GB 计费大批量
隧道一个固定入口,出口 IP 自动轮换爬虫首选

厂商一般提供 HTTP API 提取接口,典型返回:

{"code": 0, "data": [{"ip": "1.2.3.4", "port": 8080, "expire": "2026-08-17 18:00:00"}]}

提取代码(这是代理池”获取器”的原型,后面会用到):

import requests
API_URL = "https://代理厂商的提取接口?num=50&type=json"
def fetch_proxies(num=50):
"""从厂商API拉一批代理,返回 ['ip:port', ...] 列表"""
try:
r = requests.get(API_URL, params={"num": num}, timeout=15)
data = r.json()
return [f"{item['ip']}:{item['port']}" for item in data.get("data", [])]
except Exception:
return [] # 拉取失败返回空,让上层重试

4.3 自建代理:纯净但费事#

租云服务器(VPS),装代理软件(3proxysquidtinyproxy),自己维护:

  • ✅ 优点:IP 纯净(没人共享)、完全可控、能跑 SOCKS5
  • ❌ 缺点:一个 VPS 只有一个 IP、封一个少一个、要自己运维

适合”给公司内部爬虫配 3~5 个稳定出口”的场景,不适合大规模轮换。

4.4 隧道代理:懒人的生产方案#

普通代理是”一 IP 一端口”,你要自己管轮换;隧道代理是厂商给你一个固定入口(比如 tunnel.example.com:8888),每次请求经过它时,出口 IP 自动换一个

# 你的代码里永远写同一个入口,IP 却是千变万化的
proxies = {"http": "http://user:pass@tunnel.example.com:8888",
"https": "http://user:pass@tunnel.example.com:8888"}
  • ✅ 不用维护池子、不用写轮换逻辑
  • ❌ 轮换节奏不可控(不能按会话粘滞)、单价略高

🎯 选择建议:学习 → 免费代理;小项目 → 按条数买一包;中型爬虫 → 自己建代理池;大规模采集 → 隧道代理或自建池+付费混合。


5. 代理体检:先验证再上岗#

代理就像二手车,不能看广告,必须试驾。拉回来一批代理,直接塞给爬虫用,你会收获满屏超时。所以流程里必须有”体检”环节。

5.1 四个体检维度#

维度检查什么怎么查
✅ 可用性能不能连上、转发成功通过它访问 httpbin.org/ip,看返回
⚡ 速度延迟多大记录请求耗时
🥷 匿名性会不会泄露真 IPX-Forwarded-For / Via
🔌 协议支持 HTTP 还是 HTTPS分别测 http:// 和 https:// 访问

5.2 单条验证代码#

import time
import requests
def validate_proxy(proxy, timeout=8):
"""体检一个代理,返回 (可用?, 延迟, 匿名级别)"""
proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"}
try:
start = time.time()
r = requests.get("https://httpbin.org/ip",
proxies=proxies, timeout=timeout)
latency = time.time() - start
if r.status_code != 200:
return False, None, None
# 关键校验:网站看到的IP必须等于代理IP
seen = r.json().get("origin", "").split(",")[0].strip()
if seen != proxy.split(":")[0]:
return False, None, None # 代理没生效,走了直连!
# 匿名性检测
r2 = requests.get("https://httpbin.org/headers",
proxies=proxies, timeout=timeout)
h = r2.json().get("headers", {})
if "X-Forwarded-For" in h or "Via" in h:
level = "anonymous" # 匿名或透明,凑合能用但风险高
else:
level = "elite" # 高匿,合格
return True, round(latency, 2), level
except Exception:
return False, None, None

🔑 最关键的一行:校验 seen == proxy 的 IP。有相当一部分代理是”假的”——你配了它,它却把请求直连出去了(或者返回的是它自己的出口但不转发)。不校验这一步,你连”代理根本没生效”都不知道。

5.3 并发批量体检#

一条一条验太慢,50 个代理串行验要几分钟。用线程池并发验:

from concurrent.futures import ThreadPoolExecutor, as_completed
def validate_batch(proxy_list, workers=20):
"""并发体检,返回 [(proxy, latency, level), ...] 通过者列表"""
good = []
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(validate_proxy, p): p for p in proxy_list}
for f in as_completed(futures):
proxy = futures[f]
ok, latency, level = f.result()
if ok:
good.append((proxy, latency, level))
return good

💡 为什么并发体检要用线程而不是协程? 体检是 I/O 密集(大部分时间在等响应),多线程完全够用且代码简单;协程更省资源但这里没必要复杂化。这正是你之前学的并发知识在爬虫里的第一个应用场景。

5.4 体检结果的分级处理#

体检结果处理
可用 + 高匿 + 延迟 < 3s✅ 满分入池
可用 + 匿名⚠️ 入池但扣分
可用 + 透明❌ 丢弃(泄露真 IP 等于自杀)
超时/失败❌ 丢弃

6. 代理轮换:别老用一张脸敲门#

就算每个代理都合格,逮着一个用也是死路一条——那和没换代理有什么区别?**轮换(Rotation)**就是让请求均匀(或智能)地分摊到池子里所有代理上。

6.1 策略一:纯随机#

import random
pool = ["1.1.1.1:8080", "2.2.2.2:8080", "3.3.3.3:8080"]
proxy = random.choice(pool)

最简单,但纯随机不均匀——有的 IP 可能连续被抽中十次。

6.2 策略二:循环(Round-Robin)#

import itertools
pool = ["1.1.1.1:8080", "2.2.2.2:8080", "3.3.3.3:8080"]
rr = itertools.cycle(pool) # 无限循环迭代器
for _ in range(10):
proxy = next(rr) # 1,2,3,1,2,3,1,2,3,1 绝对均匀

绝对均匀,但太”规律”——网站风控如果发现三个 IP 以固定节奏交替出现,反而像机器。

6.3 策略三:按质量加权#

池子里的代理质量参差(有的 0.5 秒,有的 3 秒)。让好代理多干活、差代理少干活:

import random
# (代理, 权重),权重可以是 10/延迟 之类的分数
weighted_pool = [("1.1.1.1:8080", 10), ("2.2.2.2:8080", 6), ("3.3.3.3:8080", 3)]
proxies = [p for p, w in weighted_pool]
weights = [w for p, w in weighted_pool]
proxy = random.choices(proxies, weights=weights, k=1)[0]

6.4 策略四:失败即换(核心!)#

轮换策略里最重要的不是”怎么选”,而是”坏了怎么办”。代理平均寿命很短,请求失败的瞬间就该换一个重试:

import requests
def get_with_retry(url, pool, max_tries=3):
"""拿代理发请求,失败就换代理重试,最多试3个代理"""
for _ in range(max_tries):
proxy = random.choice(pool)
try:
r = requests.get(url,
proxies={"http": f"http://{proxy}",
"https": f"http://{proxy}"},
timeout=10)
if r.status_code == 200:
return r
except Exception:
continue # 这个代理死了,换下一个
return None # 池子里连试3个都不行

⚠️ 只对幂等请求重试:GET 请求重复发没问题;POST 提交表单就可能”提交了两次”。重试前先确认重复请求没有副作用。

6.5 会话粘滞:什么时候必须”从一而终”#

有些流程中途换 IP = 当场翻车

  • 登录 → 查订单:换了 IP,服务器认为”会话从另一个地方接入”,直接踢下线
  • 购物车 → 结算:中间换 IP,风控判定”账号被盗”,冻结
  • 分页抓取带 session 状态的接口

**粘滞(Sticky)**策略:一个任务开始前抽一个代理,整个任务期间固定用它,任务结束才还回池子。

import random
def crawl_task(urls):
"""一个任务用一个IP,从一而终"""
proxy = random.choice(pool)
proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"}
session = requests.Session() # 顺便复用TCP连接,快
for url in urls:
r = session.get(url, proxies=proxies, timeout=10)
# 处理 r ...

6.6 轮换策略选择流程图#

flowchart TD START["这个请求需要代理吗?"] --> Q1{"任务需要<br/>会话粘滞?"} Q1 -->|"是(登录/多步流程)"| STICKY["粘滞策略<br/>任务开始抽一次IP<br/>从一而终"] Q1 -->|"否(普通抓取)"| Q2{"池子多大?"} Q2 -->|"小(<10个)"| RR["循环 Round-Robin<br/>绝对均匀"] Q2 -->|"大(几十上百)"| W["按质量加权随机<br/>好代理多干活"] STICKY --> FAIL{"请求失败?"} RR --> FAIL W --> FAIL FAIL -->|"失败"| SWAP["换代理重试<br/>坏代理扣分/移除"] FAIL -->|"成功"| DONE["完成"] SWAP --> FAIL style STICKY fill:#cdf,stroke:#333,color:#222 style SWAP fill:#fcc,stroke:#333,color:#222 style DONE fill:#cfc,stroke:#333,color:#222

7. 手搓迷你代理池(单机版)#

好,零件都认识了。现在把它们拼成一台能跑的机器——迷你代理池

7.1 代理池的最小闭环#

不管多大牌的代理池,核心就四个环节:

flowchart LR F["获取器<br/>从厂商API拉代理"] --> V["验证器<br/>体检"] V -->|"合格"| S["存储<br/>SQLite/Redis"] V -->|"不合格"| X["丢弃"] S --> U["取用<br/>爬虫拿代理"] U -->|"代理挂了"| P["惩罚/移除"] P --> S style S fill:#f9f,stroke:#333,color:#222 style V fill:#cdf,stroke:#333,color:#222
  • 获取:定时从厂商 API 拉一批新代理(存量不够时补给)
  • 验证:并发体检,合格的入库
  • 存储:数据库存代理+质量分
  • 取用:爬虫按分数取代理;用坏了上报扣分

7.2 完整代码(SQLite 版,直接能跑)#

先把第 4.2 节的 fetch_proxies 和 5.3 节的 validate_batch 抄过来,再加上下面的存储层:

# mini_pool.py —— 单机版代理池,一个文件搞定
import sqlite3
import threading
import time
import random
from concurrent.futures import ThreadPoolExecutor, as_completed
# ---- 存储层:SQLite ----
class MiniPool:
def __init__(self, db="mini_pool.db"):
self.conn = sqlite3.connect(db, check_same_thread=False)
self.lock = threading.Lock()
self.conn.execute("""
CREATE TABLE IF NOT EXISTS proxies (
proxy TEXT PRIMARY KEY,
score INTEGER DEFAULT 100,
latency REAL,
level TEXT,
updated_at REAL
)""")
self.conn.commit()
def add(self, proxy, score=100, latency=0, level="elite"):
with self.lock:
self.conn.execute(
"INSERT OR REPLACE INTO proxies VALUES (?,?,?,?,?)",
(proxy, score, latency, level, time.time()))
self.conn.commit()
def get(self):
"""取一个代理:优先高分,同分随机"""
with self.lock:
cur = self.conn.execute(
"SELECT proxy FROM proxies WHERE score >= 60 "
"ORDER BY score DESC, RANDOM() LIMIT 1")
row = cur.fetchone()
return row[0] if row else None
def punish(self, proxy, penalty=20):
"""代理用坏了:扣分"""
with self.lock:
self.conn.execute(
"UPDATE proxies SET score = score - ? WHERE proxy = ?",
(penalty, proxy))
self.conn.commit()
def remove(self, proxy):
with self.lock:
self.conn.execute("DELETE FROM proxies WHERE proxy = ?", (proxy,))
self.conn.commit()
def sweep(self, min_score=40):
"""大扫除:清掉低分和过期代理"""
with self.lock:
self.conn.execute("DELETE FROM proxies WHERE score < ?", (min_score,))
self.conn.commit()
def count(self):
with self.lock:
return self.conn.execute("SELECT COUNT(*) FROM proxies").fetchone()[0]

配套的”补给 + 体检”主循环:

# 接在 mini_pool.py 后面
pool = MiniPool()
def replenish():
"""库存低于50就进货,然后批量体检入库"""
if pool.count() >= 50:
return
fresh = fetch_proxies(num=50) # 第4.2节的函数
good = validate_batch(fresh, workers=20) # 第5.3节的函数
for proxy, latency, level in good:
score = 100 if level == "elite" else 70
pool.add(proxy, score=score, latency=latency, level=level)
print(f"进货 {len(fresh)} 个,存活 {len(good)} 个,库存 {pool.count()}")
def crawl_with_pool(url):
"""爬虫侧用法:取代理 → 请求 → 失败就惩罚"""
for _ in range(3):
proxy = pool.get()
if not proxy:
replenish() # 池子空了,先补货
continue
try:
r = requests.get(url,
proxies={"http": f"http://{proxy}", "https": f"http://{proxy}"},
timeout=10)
if r.status_code == 200:
return r
except Exception:
pool.punish(proxy) # 它死了,扣分
return None
# 启动时先补一轮,之后每5分钟补一次(生产环境用定时任务)
replenish()
while True:
time.sleep(300)
replenish()
pool.sweep(min_score=40)

🧪 跑起来看效果python mini_pool.py,观察日志里”进货50个、存活30个”——免费代理的存活率就是这么惨,付费的能到 90%+。

7.3 单机版够用吗?#

场景够用吗
个人小爬虫,单进程✅ 够
学习代理池原理✅ 完美
多台机器/多进程爬虫共享代理❌ SQLite 文件锁,各玩各的
上千并发,代理每天进出几万次❌ 性能扛不住,没有监控

下一个台阶:生产级代理池


8. 生产级代理池:架构与完整实现#

8.1 单机版差在哪#

生产环境的代理池要面对:多进程爬虫同时取代理、每天几万次进出、坏代理要及时踢掉、库存不够要自动补、出了问题要有人知道。单机 SQLite 版全都不行。

生产级的升级点:

单机版生产级
SQLite 文件Redis(内存+网络共享,多机都能连)
手动启动补给调度器定时自动补给+清洗
无对外接口HTTP API(任何语言的爬虫都能用)
无监控水位/成功率指标 + 告警
分数写死动态评分:用一次扣一点、坏了狠扣、验证通过恢复

8.2 生产架构总览#

flowchart TD GET["获取器 Getter<br/>定时从厂商API进货"] --> VAL["验证器 Validator<br/>并发体检"] VAL -->|"合格"| STORE["存储 Redis<br/>ZSet:分数=质量"] VAL -->|"不合格"| TRASH["🗑️ 丢弃"] STORE --> API["API服务<br/>GET /get 取代理"] API --> SPIDER["爬虫业务<br/>(任意语言/多进程)"] SPIDER -->|"代理挂了"| BAD["DELETE /bad 上报"] BAD --> STORE SCHED["调度器 Scheduler<br/>定时: 补给/体检/清洗"] --> GET SCHED --> VAL SCHED -->|"清低分"| STORE MON["📊 监控<br/>水位/存活率/延迟<br/>不足告警"] -.-> STORE style STORE fill:#f66,stroke:#333,color:#222 style API fill:#f9f,stroke:#333,color:#222 style SCHED fill:#cdf,stroke:#333,color:#222 style MON fill:#cfc,stroke:#333,color:#222

8.3 为什么用 Redis 的 ZSet 存代理#

Redis 的 ZSet(有序集合) 天生就是为”按分数排序 + 原子操作”设计的:

  • score 字段存质量分
  • zrevrange 按分数从高到低取
  • zincrby 原子加减分(多进程并发也不会扣错)
  • zremrangebyscore 一键清理低分

动态评分规则(这是生产池的灵魂):

事件分数变化含义
新代理入库= 100初生牛犊,给个机会
被取用一次-1用得越多越”旧”,自然降权
爬虫上报坏了-30重罚,很快跌出及格线
重新体检通过恢复到 80复活机会
分数 < 50移除淘汰
# storage.py —— Redis 存储层
import redis
class RedisStorage:
def __init__(self, host="127.0.0.1", port=6379, db=0, key="proxy_pool:zset"):
self.r = redis.Redis(host=host, port=port, db=db, decode_responses=True)
self.key = key
def add(self, proxy, score=100):
"""新代理入库(重复的只更新分数)"""
self.r.zadd(self.key, {proxy: score})
def get(self):
"""取分数最高的一个,并顺手 -1 分(自然降权)"""
top = self.r.zrevrange(self.key, 0, 0)
if not top:
return None
proxy = top[0]
self.r.zincrby(self.key, -1, proxy)
return proxy
def punish(self, proxy, penalty=30):
"""爬虫上报代理挂了:狠扣"""
self.r.zincrby(self.key, -penalty, proxy)
def revive(self, proxy):
"""体检通过:分数拉回80"""
self.r.zadd(self.key, {proxy: 80})
def remove(self, proxy):
self.r.zrem(self.key, proxy)
def sweep(self, min_score=50):
"""定时大扫除"""
self.r.zremrangebyscore(self.key, "-inf", min_score)
def count(self):
return self.r.zcard(self.key)
def all_with_scores(self):
return self.r.zrevrange(self.key, 0, -1, withscores=True)

🧠 硬核点:ZSet 的原子性zincrby 是 Redis 单线程执行的原子命令。几百个爬虫进程同时扣分也不会出现”读-改-写”竞争——这正是 SQLite 锁不住的场景。

8.4 完整项目结构#

生产级代理池拆成五个模块,各干各的:

proxy_pool/
├── config.py # 配置:Redis地址、厂商API、阈值
├── getter.py # 获取器:从厂商API进货
├── validator.py # 验证器:并发体检
├── storage.py # 存储层:Redis ZSet(上面已给)
├── scheduler.py # 调度器:定时补给+清洗
├── api.py # HTTP服务:对外提供代理
└── main.py # 入口:一键启动所有模块

config.py

REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
VENDOR_API = "https://代理厂商的提取接口?num=100&type=json"
MIN_STOCK = 50 # 库存低于这个数就进货
MAX_STOCK = 200 # 库存上限
VALIDATE_WORKERS = 30 # 并发体检线程数
SWEEP_INTERVAL = 300 # 清洗间隔(秒)
GET_INTERVAL = 600 # 补给检查间隔(秒)
MIN_SCORE = 50 # 低于该分淘汰

getter.py

import requests
from config import VENDOR_API
def fetch(num=100):
"""从厂商API拉代理。失败返回空列表,调度器稍后重试"""
try:
r = requests.get(VENDOR_API, params={"num": num}, timeout=20)
data = r.json()
return [f"{x['ip']}:{x['port']}" for x in data.get("data", [])]
except Exception as e:
print(f"[getter] 拉取失败: {e}")
return []

validator.py

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from config import VALIDATE_WORKERS
def validate_one(proxy, timeout=8):
"""返回 (proxy, ok, latency, level)"""
proxies = {"http": f"http://{proxy}", "https": f"http://{proxy}"}
try:
import time
t0 = time.time()
r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=timeout)
latency = time.time() - t0
if r.status_code != 200:
return proxy, False, None, None
if r.json().get("origin", "").split(",")[0].strip() != proxy.split(":")[0]:
return proxy, False, None, None
r2 = requests.get("https://httpbin.org/headers", proxies=proxies, timeout=timeout)
h = r2.json().get("headers", {})
level = "elite" if ("X-Forwarded-For" not in h and "Via" not in h) else "anonymous"
return proxy, True, round(latency, 2), level
except Exception:
return proxy, False, None, None
def validate_batch(proxies):
"""并发体检一批,返回通过的 [(proxy, latency, level)]"""
good = []
with ThreadPoolExecutor(max_workers=VALIDATE_WORKERS) as ex:
futs = [ex.submit(validate_one, p) for p in proxies]
for f in as_completed(futs):
proxy, ok, latency, level = f.result()
if ok:
good.append((proxy, latency, level))
return good

scheduler.py —— 池子的心跳

import time
from getter import fetch
from validator import validate_batch
from storage import RedisStorage
from config import (MIN_STOCK, MAX_STOCK, MIN_SCORE,
SWEEP_INTERVAL, GET_INTERVAL)
storage = RedisStorage()
def replenish():
"""库存不够就进货 → 体检 → 入库"""
if storage.count() >= MIN_STOCK:
return
need = MAX_STOCK - storage.count()
fresh = fetch(num=need)
if not fresh:
print("[scheduler] 没拉到新货")
return
good = validate_batch(fresh)
for proxy, latency, level in good:
score = 100 if level == "elite" else 70
storage.add(proxy, score=score)
print(f"[scheduler] 进货{len(fresh)} 存活{len(good)} 库存{storage.count()}")
def run():
"""主循环:补给 + 定期体检在库代理 + 大扫除"""
last_get = last_sweep = 0
while True:
now = time.time()
if now - last_get > GET_INTERVAL:
replenish()
last_get = now
if now - last_sweep > SWEEP_INTERVAL:
# 对在库代理做一轮体检,坏的扣分,好的复活
for proxy, score in storage.all_with_scores():
_, ok, _, _ = validate_one(proxy)
if ok:
storage.revive(proxy)
else:
storage.punish(proxy, penalty=40)
storage.sweep(min_score=MIN_SCORE)
last_sweep = now
print(f"[scheduler] 清洗完成,库存{storage.count()}")
time.sleep(10)

api.py —— 对外服务(FastAPI)

# pip install fastapi uvicorn
from fastapi import FastAPI, Query
from storage import RedisStorage
app = FastAPI(title="Proxy Pool API")
storage = RedisStorage()
@app.get("/get")
def get_proxy():
"""取一个代理:爬虫每次请求前调用"""
proxy = storage.get()
if not proxy:
return {"code": 0, "msg": "池子空了,稍后再试"}
return {"code": 1, "proxy": proxy}
@app.get("/count")
def count():
return {"code": 1, "count": storage.count()}
@app.delete("/bad")
def report_bad(proxy: str = Query(..., description="挂掉的代理 ip:port")):
"""爬虫上报坏代理"""
storage.punish(proxy)
return {"code": 1, "msg": "已惩罚"}

main.py —— 一键启动

import threading
import uvicorn
from scheduler import run as scheduler_run
from api import app
if __name__ == "__main__":
# 调度器在后台线程跑
threading.Thread(target=scheduler_run, daemon=True).start()
# API 服务在前台
uvicorn.run(app, host="0.0.0.0", port=8000)

8.5 爬虫侧怎么接#

任意语言、任意进程,统一走 HTTP API:

import requests
def get_proxy():
r = requests.get("http://127.0.0.1:8000/get", timeout=3)
data = r.json()
return data["proxy"] if data["code"] == 1 else None
def report_bad(proxy):
requests.delete("http://127.0.0.1:8000/bad", params={"proxy": proxy}, timeout=3)
def crawl(url):
for _ in range(3):
proxy = get_proxy()
if not proxy:
continue
try:
r = requests.get(url,
proxies={"http": f"http://{proxy}", "https": f"http://{proxy}"},
timeout=10)
if r.status_code == 200:
return r
except Exception:
report_bad(proxy) # 坏了上报,池子会惩罚它
return None

8.6 监控与告警:别等池子干了才发现#

生产系统最大的坑:池子静悄悄干涸,爬虫集体 403,你还不知道。必须监控:

指标怎么看告警阈值示例
池子水位storage.count()< 20 告警
进货存活率存活/进货< 50% 说明供应商质量问题
平均延迟体检时记录> 3s 告警
API 请求量日志统计突降说明爬虫出问题

轻量版告警(不引入监控系统,定时检查+钉钉/邮件通知):

# monitor.py —— 每5分钟查一次水位,低了就喊人
import time, requests
DINGTALK_WEBHOOK = "https://oapi.dingtalk.com/robot/send?access_token=你的token"
def check_and_alert():
r = requests.get("http://127.0.0.1:8000/count", timeout=5)
count = r.json()["count"]
if count < 20:
requests.post(DINGTALK_WEBHOOK, json={
"msgtype": "text",
"text": {"content": f"⚠️ 代理池水位告警: 当前仅剩 {count} 个代理!"}
})
return count
while True:
try:
print(f"[monitor] 水位 {check_and_alert()}")
except Exception as e:
print(f"[monitor] 检查失败: {e}")
time.sleep(300)

📈 再往上:公司级方案是把指标暴露成 Prometheus 格式,用 Grafana 画仪表盘。原理就是上面这几个数字,只是采集和展示更专业。


9. 踩坑指南与合规红线#

9.1 六个经典坑#

症状解法
代理超时太短好代理被误杀代理超时 ≥ 直连超时×2
只配 http 不配 httpsHTTPS 请求裸奔直连,真 IP 泄露两个都配,缺一不可
池子空了不降级爬虫全部停摆空池时允许短暂直连+告警
POST 盲目重试数据提交两次只对幂等请求重试
免费代理传账号密码密码被肉鸡截获免费代理别碰敏感数据
换了代理忘了 session会话状态丢失粘滞策略(见 6.5)

9.2 合规红线(认真看)#

技术是中性的,用法分对错。用代理做爬虫,至少守住这些:

  1. 看 robots.txt:网站明令禁止的目录别爬
  2. 看服务条款:写了”禁止自动化采集”的平台,硬爬=违约
  3. 控制频率:代理是让你”礼貌地分散请求”,不是”肆无忌惮地轰炸”
  4. 别碰红线数据:个人信息、付费内容、内部数据——碰了就不是技术问题了
  5. 别干扰服务:并发打满对方服务器=破坏计算机信息系统,真会坐牢的

🎓 记住:代理池的价值是让”合规采集”更稳定,而不是让”违规采集”更隐蔽。前者是工程,后者是风险。


10. 学习路线总结#

flowchart TD L1["入门:requests 加 proxies 参数<br/>httpbin.org 验证生效"] --> L2["原理:HTTP转发 vs CONNECT隧道<br/>透明/匿名/高匿"] L2 --> L3["体检:可用性/速度/匿名性<br/>并发验证"] L3 --> L4["轮换:随机/循环/加权<br/>失败即换+会话粘滞"] L4 --> L5["单机池:获取+验证+存储+取用<br/>SQLite闭环"] L5 --> L6["生产池:Redis ZSet评分<br/>API服务+调度器+监控"] style L1 fill:#cfc,stroke:#333,color:#222 style L3 fill:#ffd,stroke:#333,color:#222 style L5 fill:#ffd,stroke:#333,color:#222 style L6 fill:#f66,stroke:#333,color:#222

动手作业(由易到难):

  1. 买 1 块钱的付费代理试用包,用第 2 节的代码验证是否生效
  2. 用免费代理跑第 5 节的体检脚本,看存活率有多惨
  3. 把第 7 节的 mini_pool.py 跑起来,接到你自己写的爬虫上
  4. 装个 Redis,把第 8 节的完整项目跑通,用 /get/bad 接口
  5. 给你的代理池加监控告警,故意把水位打低,看告警能不能响
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

IP代理池的实现
https://fatdog.20060113.xyz/posts/ip-cool/
作者
神秘大胖狗
发布于
2026-08-17
许可协议
MIT

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00