Python 爬虫代理实战 —— 从一行代码到生产级代理池 🕵️
每个爬虫人的成人礼:兴冲冲跑通爬虫,第二天再跑,满屏 403。 本篇从
requests加一行参数讲起,一路升级到能扛生产流量的代理池。由简到难,边讲边搓。
目录
- 为什么需要代理:IP 被封的那一天
- 代理入门:requests 加一行参数
- 硬核原理:代理是怎么替你发请求的
- 代理从哪来:免费、付费、自建、隧道
- 代理体检:先验证再上岗
- 代理轮换:别老用一张脸敲门
- 手搓迷你代理池(单机版)
- 生产级代理池:架构与完整实现
- 踩坑指南与合规红线
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 各库的代理写法速查
| 库 | 写法 | 备注 |
|---|---|---|
requests | requests.get(url, proxies={"http": p, "https": p}) | 最常用 |
urllib | build_opener(ProxyHandler({"http": p})) | 标准库,写法啰嗦 |
httpx | httpx.Client(proxy=p) | 同步异步通吃 |
aiohttp | session.get(url, proxy=p) | 异步首选 |
# httpx 同步版import httpxwith 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 为什么 http 和 https 要分开写
因为走代理时,HTTP 和 HTTPS 的工作方式完全不同(下一节详细讲原理):
http://开头的代理 → HTTP 请求走”明文转发”https://开头的代理 → HTTPS 请求走”CONNECT 隧道”
所以一个代理可能支持 HTTP 转发、不支持 HTTPS 隧道。分开写,requests 才知道遇到哪种请求该找哪个代理。如果你只写一个,遇到另一种协议就会绕过代理直连——小心 IP 泄露!
3. 硬核原理:代理是怎么替你发请求的
这节是全篇最硬核的部分,看懂它你就理解了代理的一切怪毛病。
3.1 HTTP 代理:明文转发
访问 HTTP 网站时,请求本身是明文。代理收到请求后直接替你转发即可:
注意细节:发给代理的请求行带完整 URL(GET http://目标站/页面),而代理转发给目标站时只发路径(GET /页面)。这就是代理和”直连”在 HTTP 报文层面唯一的区别。
3.2 HTTPS 代理:CONNECT 隧道
访问 HTTPS 网站时,内容全程加密。代理看不见、也不该看见内容。那它怎么转发?答案是先开一条”盲管道”:
CONNECT 方法(RFC 7231)是 HTTPS 代理的核心:
- 客户端先发
CONNECT 目标站:443,意思是”给我开条到这里的管道” - 代理连上目标站后回
200 Connection Established - 之后代理变成纯搬运工——收到的每个字节原样转发,不解密、不修改
所以: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),装代理软件(3proxy、squid、tinyproxy),自己维护:
- ✅ 优点: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,看返回 |
| ⚡ 速度 | 延迟多大 | 记录请求耗时 |
| 🥷 匿名性 | 会不会泄露真 IP | 查 X-Forwarded-For / Via 头 |
| 🔌 协议 | 支持 HTTP 还是 HTTPS | 分别测 http:// 和 https:// 访问 |
5.2 单条验证代码
import timeimport 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 轮换策略选择流程图
7. 手搓迷你代理池(单机版)
好,零件都认识了。现在把它们拼成一台能跑的机器——迷你代理池。
7.1 代理池的最小闭环
不管多大牌的代理池,核心就四个环节:
- 获取:定时从厂商 API 拉一批新代理(存量不够时补给)
- 验证:并发体检,合格的入库
- 存储:数据库存代理+质量分
- 取用:爬虫按分数取代理;用坏了上报扣分
7.2 完整代码(SQLite 版,直接能跑)
先把第 4.2 节的 fetch_proxies 和 5.3 节的 validate_batch 抄过来,再加上下面的存储层:
# mini_pool.py —— 单机版代理池,一个文件搞定import sqlite3import threadingimport timeimport randomfrom 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 生产架构总览
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 requestsfrom 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 requestsfrom concurrent.futures import ThreadPoolExecutor, as_completedfrom 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 goodscheduler.py —— 池子的心跳
import timefrom getter import fetchfrom validator import validate_batchfrom storage import RedisStoragefrom 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 uvicornfrom fastapi import FastAPI, Queryfrom 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 threadingimport uvicornfrom scheduler import run as scheduler_runfrom 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 None8.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 不配 https | HTTPS 请求裸奔直连,真 IP 泄露 | 两个都配,缺一不可 |
| 池子空了不降级 | 爬虫全部停摆 | 空池时允许短暂直连+告警 |
| POST 盲目重试 | 数据提交两次 | 只对幂等请求重试 |
| 免费代理传账号密码 | 密码被肉鸡截获 | 免费代理别碰敏感数据 |
| 换了代理忘了 session | 会话状态丢失 | 粘滞策略(见 6.5) |
9.2 合规红线(认真看)
技术是中性的,用法分对错。用代理做爬虫,至少守住这些:
- 看 robots.txt:网站明令禁止的目录别爬
- 看服务条款:写了”禁止自动化采集”的平台,硬爬=违约
- 控制频率:代理是让你”礼貌地分散请求”,不是”肆无忌惮地轰炸”
- 别碰红线数据:个人信息、付费内容、内部数据——碰了就不是技术问题了
- 别干扰服务:并发打满对方服务器=破坏计算机信息系统,真会坐牢的
🎓 记住:代理池的价值是让”合规采集”更稳定,而不是让”违规采集”更隐蔽。前者是工程,后者是风险。
10. 学习路线总结
动手作业(由易到难):
- 买 1 块钱的付费代理试用包,用第 2 节的代码验证是否生效
- 用免费代理跑第 5 节的体检脚本,看存活率有多惨
- 把第 7 节的
mini_pool.py跑起来,接到你自己写的爬虫上 - 装个 Redis,把第 8 节的完整项目跑通,用
/get和/bad接口 - 给你的代理池加监控告警,故意把水位打低,看告警能不能响
部分信息可能已经过时









