做爬虫的朋友大概率都遇到过这种场景:单线程跑着还行,一上多线程,目标站点直接给你403,或者IP被标记成异常流量,接下来几分钟内你所有请求全被拒。你换台机器、换个IP,又好了。这时候你就明白,多线程爬虫和代理ip池,基本是绑在一起的。没有ip池,你的线程数上不去;有了ip池但没搭好,线程越多死得越快。
这篇文章不聊什么高深的分布式架构,就从一个实际项目的角度,把"多线程爬虫怎么配代理ip池"这件事从头到尾捋一遍。你跟着做,基本就能跑起来。
先想明白:多线程为什么必须配代理ip
说白了,多线程爬虫的本质就是"同一时间从多个出口发请求"。你开10个线程,目标服务器那边看到的就是10个并发连接。如果这10个连接全从你本机同一个IP出去,对方风控系统三秒钟就能判定你是机器行为,直接封你。
那代理ip解决的就是这个问题——让每个线程走不同的出口IP。10个线程配10个不同的代理IP,在目标服务器看来,这就是10个不同用户在不同网络环境下正常访问,风控根本不会触发。
但这里有个关键细节:不是随便找个代理就能用。你用的IP如果已经被别人用烂了,目标站点早就把它拉黑了,你换了个"代理"等于没换。所以IP的纯净度和新鲜度,比数量更重要。这也是后面选服务商时要重点看的。
代理ip池的架构,拆开看其实就三层
很多人一听到"ip池"就觉得是个很复杂的东西,其实你把它拆成三层就清楚了:
第一层:IP资源层。这就是你的IP来源。可以是自己抓的免费代理(不推荐,质量太差),也可以是商业代理服务商提供的IP池。商业IP池的优势在于,人家有运营商授权,IP是真实用户网络出口,纯净度高,而且会持续更新去重。你不用自己维护,调个API就能拿到可用IP。
第二层:调度层。这一层负责"谁在什么时候用哪个IP"。你的爬虫线程要发请求了,调度层就从池子里挑一个合适的IP给它。挑的逻辑可以是轮询、随机、按地区匹配、按剩余有效期筛选,看你的业务需求定。
第三层:健康检测层。IP不是拿到就永远能用的。短效IP可能几分钟就过期了,网络抖动也可能让某个IP暂时不通。健康检测层定期(或者在请求失败时)验证IP是否还活着,把死的踢出去,保证池子里都是能用的。
这三层的关系很简单:资源层提供"弹药",调度层负责"分发",健康检测层负责"淘汰不合格的"。你搭池子的时候,把这三件事想清楚,代码结构就不会乱。
选IP代理服务商,别光盯着单价
市面上做代理IP的服务商不少,价格从几块钱到几十块钱一个IP不等。但我的建议是,别只看单价,要看下面这几个指标:
| 关注点 | 为什么重要 | 怎么判断 |
|---|---|---|
| IP来源是否正规 | 非正规来源的IP随时可能被运营商封禁,你用到一半突然全挂了 | 看是否拿到运营商授权,资源量级是否稳定 |
| IP纯净度/可用率 | 一个IP如果已经被大量爬虫用过,目标站点大概率已经标记了 | 问清楚去重机制,看可用率承诺(99%以上才算靠谱) |
| IP时效与更新频率 | 短效IP如果更新不及时,你池子里一半都是过期的 | 看每日更新量,短效IP的有效期是否灵活可配 |
| 并发提取能力 | 多线程同时要IP,如果API响应慢,你的线程全在排队等IP | 压测一下API的QPS,看高并发下延迟多少 |
| 协议支持 | 有些目标站点只走HTTPS,你的代理如果只支持HTTP就白搭 | 确认是否支持HTTP/HTTPS/SOCKS5 |
我目前项目里用的是神龙HTTP,简单说下为什么选它。首先是IP来源这块,它是国内三大运营商正规授权的,资源储备在3000万+这个量级,而且每日更新去重,IP纯净度标称99.8%。这个数据意味着什么?你从池子里随机抽一个IP,大概率是"干净"的,目标站点不会一上来就给你403。
第二个点是它的IP时效比较灵活。短效动态IP池支持3/5/10/15/30分钟不同有效期,你可以根据目标站点的封禁策略来选。比如某个站封IP比较快,你就用3分钟的短效IP,用完即弃,线程自动换下一个。如果是做市场研究这类对IP稳定性要求没那么极端的场景,长效静态IP池(1到24小时)性价比更高,不用频繁换IP。
另外它的API接口对主流爬虫语言都兼容,Python、Java、Go都能直接调,不用你额外写适配层。文档里也有现成的示例代码,集成成本很低。后面搭池子的时候我会用它的API来演示。
动手搭:一个能跑的多线程代理ip池
下面用Python写一个最小可用的多线程代理ip池。逻辑是:从神龙HTTP的API拉取IP → 放进队列 → 工作线程从队列取IP发请求 → 请求失败则丢弃该IP并补一个新的。
import requests
import threading
import queue
import time
import random
============ 配置区 ============
API_URL = "https://api.shenlongip.com/getip" 神龙HTTP取IP接口
API_KEY = "你的API密钥"
PROXY_TYPE = "http" http / https / socks5
THREAD_COUNT = 10 工作线程数
POOL_SIZE = 50 池子容量
IP_TTL = 300 IP有效期(秒),短效IP建议设短一点
============ IP池管理 ============
class ProxyPool:
def __init__(self, pool_size=50):
self.pool = queue.Queue(maxsize=pool_size)
self.lock = threading.Lock()
self.failed_ips = set() 记录失败的IP,短期内不再使用
self.failed_expire = {} IP -> 过期时间戳
def fetch_ip(self):
"""从神龙HTTP API获取一个可用IP"""
try:
resp = requests.get(
API_URL,
params={"key": API_KEY, "type": PROXY_TYPE, "count": 1},
timeout=5
)
data = resp.json()
if data.get("code") == 0 and data.get("data"):
ip_info = data["data"][0]
proxy = f"{PROXY_TYPE}://{ip_info['ip']}:{ip_info['port']}"
return proxy
except Exception as e:
print(f"[WARN] 获取IP失败: {e}")
return None
def get_proxy(self):
"""从池中取一个IP,池空则补货"""
while True:
try:
proxy = self.pool.get_nowait()
检查是否还在黑名单里
with self.lock:
if proxy in self.failed_ips:
expire_time = self.failed_expire.get(proxy, 0)
if time.time() < expire_time:
continue 还在冷却期,跳过
else:
self.failed_ips.discard(proxy)
self.failed_expire.pop(proxy, None)
return proxy
except queue.Empty:
池子空了,补一批
print("[INFO] 池子空了,补充IP...")
for _ in range(POOL_SIZE):
ip = self.fetch_ip()
if ip:
self.pool.put(ip)
time.sleep(1) 避免瞬间打爆API
def mark_failed(self, proxy):
"""标记IP失败,冷却10分钟后允许重新使用"""
with self.lock:
self.failed_ips.add(proxy)
self.failed_expire[proxy] = time.time() + 600
def health_check(self):
"""后台线程:定期验证池中IP是否还活着"""
while True:
time.sleep(30) 每30秒检查一次
current_size = self.pool.qsize()
for _ in range(current_size):
try:
proxy = self.pool.get_nowait()
简单验证:用代理访问一个轻量页面
try:
r = requests.get(
"http://httpbin.org/ip",
proxies={"http": proxy, "https": proxy},
timeout=5
)
if r.status_code == 200:
self.pool.put(proxy) 活着,放回池子
else:
self.mark_failed(proxy)
except:
self.mark_failed(proxy)
except queue.Empty:
break
============ 工作线程 ============
def worker(pool: ProxyPool, task_queue: queue.Queue):
"""每个线程:取IP -> 执行任务 -> 失败则换IP重试"""
while True:
try:
task = task_queue.get(timeout=5)
except queue.Empty:
continue
max_retry = 3
for attempt in range(max_retry):
proxy = pool.get_proxy()
try:
这里替换成你实际的爬取逻辑
resp = requests.get(
task,
proxies={"http": proxy, "https": proxy},
timeout=10,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
}
)
if resp.status_code == 200:
处理数据...
print(f"[OK] {task} via {proxy}")
break
elif resp.status_code in (403, 429, 503):
被风控了,换IP重试
pool.mark_failed(proxy)
print(f"[RETRY] {proxy} 被拒,换IP重试 ({attempt+1}/{max_retry})")
time.sleep(random.uniform(0.5, 2))
else:
print(f"[WARN] {task} 返回 {resp.status_code}")
break
except requests.exceptions.ProxyError:
pool.mark_failed(proxy)
print(f"[RETRY] {proxy} 代理错误,换IP ({attempt+1}/{max_retry})")
time.sleep(1)
except Exception as e:
print(f"[ERROR] {e}")
break
============ 主入口 ============
def main():
pool = ProxyPool(pool_size=POOL_SIZE)
启动健康检测后台线程
checker = threading.Thread(target=pool.health_check, daemon=True)
checker.start()
准备任务队列(这里用示例URL,实际替换成你的目标)
task_queue = queue.Queue()
for i in range(100):
task_queue.put(f"https://example.com/page/{i}")
启动工作线程
threads = []
for i in range(THREAD_COUNT):
t = threading.Thread(target=worker, args=(pool, task_queue), daemon=True)
t.start()
threads.append(t)
time.sleep(0.2) 错开启动,避免瞬间并发打满
print(f"[START] {THREAD_COUNT} 个线程已启动,池子容量 {POOL_SIZE}")
主线程等待
while not task_queue.empty():
time.sleep(2)
print("[DONE] 所有任务完成")
if __name__ == "__main__":
main()
这段代码的核心逻辑你重点看三个地方:
第一,ProxyPool的get_proxy方法。它不是简单地从队列里弹一个出来就完事,而是会检查这个IP是不是在"失败黑名单"里。如果某个IP刚被目标站点403了,你马上又拿它去请求,大概率还是403。所以设了一个10分钟的冷却期,冷却期内这个IP不会被分配出去。
第二,worker里的重试机制。一个任务最多重试3次,每次重试都换一个全新的IP。注意这里不是换端口,是换整个IP。因为403/429这种响应,说明对方封的是你这个IP,换端口没意义。
第三,health_check后台线程。它每30秒把池子里的IP都验证一遍,死的踢掉。这个很重要,尤其是你用短效IP的时候,3分钟、5分钟就过期了,如果不主动检测,你的线程会频繁拿到过期IP,白白浪费重试次数。
IP轮换策略:别把IP用废了
很多新手搭完池子,线程一跑起来,IP消耗速度远超预期。一个短效IP3分钟就过期了,你10个线程每个请求间隔2秒,理论上3分钟能发90个请求,但实际可能5分钟就把池子耗空了。怎么控制?
策略一:按线程绑定IP,而不是按请求绑定。 每个工作线程在一段时间内(比如60秒)固定用一个IP,60秒后再换。这样你的IP消耗速度是"线程数 × 每60秒换一次",而不是"每个请求换一个"。对于大多数场景,一个IP在60秒内发几十个请求,目标站点不会觉得异常。
策略二:请求间隔加随机抖动。 别让你的线程以完全固定的频率发请求。每个请求之间加一个0.5到3秒的随机sleep,模拟人类操作的不规则性。代码里就是 time.sleep(random.uniform(0.5, 3)),看着不起眼,但对降低被风控的概率帮助很大。
策略三:根据目标站点调整IP有效期。 如果你爬的是对IP敏感的大站,用神龙HTTP的3分钟或5分钟短效IP,用完即弃。如果是行业垂直网站、数据接口这类对IP没那么敏感的目标,用长效静态IP(1小时到24小时)就够了,省IP也省成本。固定IP池适合那种"我就需要一两个稳定出口,长期跑"的场景,按个数买,包时计费,单价其实不贵。
还有一个容易被忽略的点:请求头别太"干净"。你用了代理IP,但User-Agent还是Python-requests/2.28,Accept-Encoding还是gzip,这跟真实浏览器差太远了。至少把UA、Accept、Accept-Language这几个头设成正常浏览器的值。我上面代码里已经加了,你实际用的时候根据目标站点调整。
几个踩坑后总结的经验
坑一:API取IP的并发别太高。 你10个线程同时去调神龙HTTP的取IP接口,如果池子刚好空了,10个请求同时打过去,API那边可能有速率限制。建议取IP操作加个锁或者用单线程统一补货,工作线程只从本地队列取,不直接调API。我上面代码里get_proxy方法里池子空了才补货,而且补货是串行的,就是为了避免这个问题。
坑二:HTTPS目标站点,代理协议要对。 如果你的目标站点是HTTPS的,你的代理必须支持HTTPS。神龙HTTP是支持HTTP/HTTPS/SOCKS5三种协议的,你在取IP的时候指定type参数就行。别用HTTP代理去连HTTPS站点,会直接握手失败。
坑三:别忽略IP的地域属性。 有些业务场景需要特定地区的IP。比如你做市场调研,想看某个城市本地的搜索结果,那IP就得是那个城市的。神龙HTTP支持300+城市级精准定位,取IP的时候可以指定省份或城市。如果你的场景不需要地域区分,就选混播,池子利用率更高。
坑四:日志一定要打。 多线程环境下,问题排查全靠日志。每个请求记录:用的哪个IP、目标URL、响应状态码、耗时。跑完之后你一看日志就知道哪些IP质量差、哪些时间段失败率高,方便你调整策略。神龙HTTP的个人中心也有可视化的使用数据统计,能看到IP使用趋势和异常,配合你自己的日志一起看,问题定位会快很多。
常见问题
Q1:我线程数开多少合适?跟IP池大小什么关系?
经验法则:线程数 ≤ 池子容量 / 3。比如你池子50个IP,线程开10到15个比较稳。线程太多,IP消耗太快,池子频繁补货,API压力也大。线程太少,并发上不去,效率低。具体数值你根据目标站点的响应速度和你的IP有效期来调。一般先开10个线程跑,观察IP消耗速率和失败率,再增减。
Q2:短效IP和长效IP,我到底该选哪个?
看你的目标站点"记仇"程度。如果目标站点封IP很积极(比如你请求20次就封),用短效动态IP,3到5分钟一换,它封你的时候你已经换下一个了。如果目标站点比较宽松,或者你爬的是API接口、数据源这类不怎么看IP的地方,用长效静态IP(1小时以上)更划算,IP消耗少,成本也低。神龙HTTP两种都有,而且有效期可以定制,你按实际需求选就行。
Q3:我的爬虫跑着跑着突然全部403了,怎么排查?
先别慌,按这个顺序查:第一,看是不是你的IP池里大部分IP都过期了(短效IP没及时更新),去神龙HTTP后台看下IP状态;第二,看是不是目标站点临时加了更严格的风控(比如突然要求JS验证),换个IP试一下,如果所有IP都403,大概率是站点策略变了,不是IP的问题;第三,看你的请求频率是不是突然飙高了(比如某个循环没加sleep),把频率降下来观察。如果以上都排除了,联系神龙HTTP的技术支持,他们7×24在线,可以帮你查具体IP的状态。
Q4:池子里的IP用完了,补货要等很久,这段时间线程就干等着?
两个办法。一是把池子容量设大一点,比如100个,补货的间隔就会拉长,不容易出现"池子空了"的情况。二是在get_proxy方法里,池子空的时候不要死等,可以设一个超时(比如5秒),超时后线程先sleep一小段再重试,避免所有线程同时疯狂调API补货。神龙HTTP的API本身并发提取能力是够的,正常补货速度不会太慢,真正卡住的情况一般是你的网络到API之间有问题,检查一下就行。


