在高强度的爬虫和逆向分析场景中,请求的 IP 地址是识别和封禁爬虫流量的第一道关卡。相比于易于被识别和屏蔽的数据中心 IP,动态变化的住宅 IP 地址具有极高的伪装度,是绕过反爬虫策略的关键技术。本节将详细介绍动态住宅 IP(拨号代理)的原理及其代理池的搭建方法。
1. 动态住宅 IP (拨号代理) 原理
a) 什么是动态住宅 IP?
住宅 IP: 指由互联网服务提供商(ISP),如电信、联通、移动,分配给普通家庭用户的 IP 地址。这些 IP 地址属于真实的住宅网络,信誉度最高。
动态 IP: 与数据中心固定的静态 IP 不同,住宅宽带通常使用 PPPoE (Point-to-Point Protocol over Ethernet) 协议进行拨号上网。其核心特点是:每断开一次连接再重新拨号,ISP 的 BRAS(宽带远程接入服务器)就会从其地址池中重新分配一个新的 IP 地址给用户。
# 允许所有来源的所有请求
http_access allow all
# 监听端口
http_port 3128
# 禁止泄露原始 IP
forwarded_for off
request_header_access Via deny all
request_header_access X-Forwarded-For deny all
c) 管理层与调度
当你有大量的拨号节点时,一个中心化的管理系统是必不可少的。
中心 API 服务器:
IP 注册: 每个拨号节点在成功获取新 IP 后,将 (新IP:端口, 地理位置, ISP) 等信息上报给中心服务器。
IP 获取: 业务程序(如爬虫)通过调用 API,从中心服务器获取一个当前可用的代理 IP。可以根据需求指定地理位置等条件。
IP 续期与心跳: 拨号节点需要定期向中心服务器发送心跳,证明自己仍然在线。如果心跳超时,服务器就将该 IP 从可用池中移除。
IP 池管理策略:
可用性检测: 中心服务器定期主动检测池中代理的连通性,剔除失效的 IP。
IP 轮换: 当一个 IP 被封禁或使用次数过多时,业务程序可以调用 API 请求中心服务器通知对应的拨号节点执行"换 IP"操作。
并发控制: 管理每个代理 IP 当前的并发请求数,避免因过度使用而被封禁。
d) 整体架构图
graph TD
subgraph "业务服务器"
A[爬虫/业务应用] --> B{中心 API Server}
end
B -- 获取代理 --> A
B -- 管理/调度 --> C1
B -- 管理/调度 --> C2
B -- 管理/调度 --> C3
subgraph "拨号节点 1 (上海电信)"
C1[树莓派] --> D1[代理服务 Squid]
C1 -- 控制重拨 --> E1[路由器/光猫]
E1 -- PPPoE --> F1[(ISP 网络)]
end
subgraph "拨号节点 2 (北京联通)"
C2[树莓派] --> D2[代理服务 Squid]
C2 -- 控制重拨 --> E2[路由器/光猫]
E2 -- PPPoE --> F2[(ISP 网络)]
end
subgraph "拨号节点 N (深圳移动)"
C3[树莓派] --> D3[代理服务 Squid]
C3 -- 控制重拨 --> E3[路由器/光猫]
E3 -- PPPoE --> F3[(ISP 网络)]
end
C1 -- 上报 IP --> B
C2 -- 上报 IP --> B
C3 -- 上报 IP --> B
graph TD
subgraph "业务服务器"
A[爬虫/业务应用] --> B{中心 API Server}
end
B -- 获取代理 --> A
B -- 管理/调度 --> C1
B -- 管理/调度 --> C2
B -- 管理/调度 --> C3
subgraph "拨号节点 1 (上海电信)"
C1[树莓派] --> D1[代理服务 Squid]
C1 -- 控制重拨 --> E1[路由器/光猫]
E1 -- PPPoE --> F1[(ISP 网络)]
end
subgraph "拨号节点 2 (北京联通)"
C2[树莓派] --> D2[代理服务 Squid]
C2 -- 控制重拨 --> E2[路由器/光猫]
E2 -- PPPoE --> F2[(ISP 网络)]
end
subgraph "拨号节点 N (深圳移动)"
C3[树莓派] --> D3[代理服务 Squid]
C3 -- 控制重拨 --> E3[路由器/光猫]
E3 -- PPPoE --> F3[(ISP 网络)]
end
C1 -- 上报 IP --> B
C2 -- 上报 IP --> B
C3 -- 上报 IP --> B
graph TD
subgraph "业务服务器"
A[爬虫/业务应用] --> B{中心 API Server}
end
B -- 获取代理 --> A
B -- 管理/调度 --> C1
B -- 管理/调度 --> C2
B -- 管理/调度 --> C3
subgraph "拨号节点 1 (上海电信)"
C1[树莓派] --> D1[代理服务 Squid]
C1 -- 控制重拨 --> E1[路由器/光猫]
E1 -- PPPoE --> F1[(ISP 网络)]
end
subgraph "拨号节点 2 (北京联通)"
C2[树莓派] --> D2[代理服务 Squid]
C2 -- 控制重拨 --> E2[路由器/光猫]
E2 -- PPPoE --> F2[(ISP 网络)]
end
subgraph "拨号节点 N (深圳移动)"
C3[树莓派] --> D3[代理服务 Squid]
C3 -- 控制重拨 --> E3[路由器/光猫]
E3 -- PPPoE --> F3[(ISP 网络)]
end
C1 -- 上报 IP --> B
C2 -- 上报 IP --> B
C3 -- 上报 IP --> B
graph TD
subgraph "业务服务器"
A[爬虫/业务应用] --> B{中心 API Server}
end
B -- 获取代理 --> A
B -- 管理/调度 --> C1
B -- 管理/调度 --> C2
B -- 管理/调度 --> C3
subgraph "拨号节点 1 (上海电信)"
C1[树莓派] --> D1[代理服务 Squid]
C1 -- 控制重拨 --> E1[路由器/光猫]
E1 -- PPPoE --> F1[(ISP 网络)]
end
subgraph "拨号节点 2 (北京联通)"
C2[树莓派] --> D2[代理服务 Squid]
C2 -- 控制重拨 --> E2[路由器/光猫]
E2 -- PPPoE --> F2[(ISP 网络)]
end
subgraph "拨号节点 N (深圳移动)"
C3[树莓派] --> D3[代理服务 Squid]
C3 -- 控制重拨 --> E3[路由器/光猫]
E3 -- PPPoE --> F3[(ISP 网络)]
end
C1 -- 上报 IP --> B
C2 -- 上报 IP --> B
C3 -- 上报 IP --> B
fromflaskimportFlask,request,jsonifyfromdatetimeimportdatetime,timedeltaimportthreadingimporttimeapp=Flask(__name__)# IP 池存储ip_pool={}lock=threading.Lock()@app.route('/register',methods=['POST'])defregister():"""注册新 IP"""data=request.jsonwithlock:ip_pool[data['node_id']]={'ip':data['ip'],'port':data['port'],'location':data['location'],'isp':data['isp'],'last_heartbeat':datetime.now(),'in_use':False}returnjsonify({'status':'ok'})@app.route('/heartbeat',methods=['POST'])defheartbeat():"""接收心跳"""data=request.jsonwithlock:ifdata['node_id']inip_pool:ip_pool[data['node_id']]['last_heartbeat']=datetime.now()returnjsonify({'status':'ok'})@app.route('/get_proxy',methods=['GET'])defget_proxy():"""获取可用代理"""location=request.args.get('location')isp=request.args.get('isp')withlock:fornode_id,infoinip_pool.items():# 检查是否超时ifdatetime.now()-info['last_heartbeat']>timedelta(seconds=30):continue# 检查是否被占用ifinfo['in_use']:continue# 匹配条件iflocationandinfo['location']!=location:continueifispandinfo['isp']!=isp:continue# 标记为使用中info['in_use']=Truereturnjsonify({'proxy':f"{info['ip']}:{info['port']}",'node_id':node_id,'location':info['location'],'isp':info['isp']})returnjsonify({'error':'No available proxy'}),404@app.route('/release_proxy',methods=['POST'])defrelease_proxy():"""释放代理"""data=request.jsonwithlock:ifdata['node_id']inip_pool:ip_pool[data['node_id']]['in_use']=Falsereturnjsonify({'status':'ok'})if__name__=='__main__':app.run(host='0.0.0.0',port=5000)