随着大语言模型(LLM)参数量飙升到万亿级别,高质量的公共数据已经被各家大厂“刮地三尺”。想要获取更垂直、更新鲜的行业数据,必须深入互联网的毛细血管。但在严苛的限制机制下,单机爬虫面临着算力瓶颈和极易被限制的死局。
破局的最优解只有一个:Redis分布式任务队列 + 多线程并发 + 动态代理IP池。
为什么是 Redis?分布式爬虫的“最强大脑”
在分布式架构中,我们需要多台服务器(Worker)同时去抓取数据。这就引出了一个核心问题:如何保证大家不抓重复的网页?又如何把成千上万的URL分配给不同的机器?
这就是 Redis 发挥作用的地方:
- 任务分发(中央调度): 我们可以把 Redis 的
List当作一个巨大的任务队列。一台主服务器(Master)负责把需要抓取的 URL 塞进队列,其他所有的爬虫服务器(Worker)都盯着这个队列,谁有空谁就去“抢”一个 URL 来抓。 - 极高的读写性能: Redis 基于内存操作,能够轻松扛住几万甚至十几万的并发读写,绝不会成为爬虫的瓶颈。
架构升级:多线程 + Redis + 动态代理
在这个架构中,每台 Worker 机器不仅要从 Redis 抢任务,还要在自己机器上开启多线程来最大化压榨 CPU 和网络带宽。同时,为了防止单台 Worker 触发目标网站的反爬策略,我们必须给每个线程挂上动态代理IP。
下面是结合了 Redis 队列、Python concurrent.futures 线程池以及动态代理的实战代码:
import requests
import redis
import time
from concurrent.futures import ThreadPoolExecutor
from requests.exceptions import RequestException
# ==========================================
# 16YUN爬虫代理配置信息 (请替换为实际账户)
# ==========================================
PROXY_HOST = "proxy.16yun.cn" # 代理服务器域名
PROXY_PORT = "31111" # 代理服务器端口
PROXY_USER = "16YUNxxxx" # 代理用户名 (16YUN开头)
PROXY_PASS = "YOUR_PASSWORD" # 代理密码
# ==========================================
# Redis 配置信息
# ==========================================
REDIS_HOST = 'localhost' # Redis服务器IP,实际分布式部署时填公网或内网IP
REDIS_PORT = 6379 # Redis端口
REDIS_QUEUE_NAME = 'crawler:url_queue' # 存放任务的队列名称
# 初始化 Redis 连接池
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
def fetch_data(url):
"""
Worker 线程执行的具体抓取任务
每次请求都会通过亿牛云爬虫代理自动切换IP
"""
proxies = {
"http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
"https": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
}
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",
"Connection": "keep-alive"
}
try:
print(f"[*] 线程启动,正在通过代理抓取: {url}")
# 设置 timeout 防止死链卡住线程
response = requests.get(url, headers=headers, proxies=proxies, timeout=10)
if response.status_code == 200:
print(f"[+] 抓取成功: {url} | 响应截取: {response.text[:50]}...")
# 注意:实际业务中这里会把清洗好的数据存入 MongoDB 或 Elasticsearch
return True
else:
print(f"[-] 抓取失败 {url},状态码: {response.status_code}")
# 容错处理:抓取失败的 URL 可以重新塞回 Redis 队列末尾重试
# redis_client.rpush(REDIS_QUEUE_NAME, url)
return False
except RequestException as e:
print(f"[-] 请求异常 {url}: {e}")
# redis_client.rpush(REDIS_QUEUE_NAME, url)
return False
def worker_process():
"""
Worker 节点的主进程:不断从 Redis 拉取任务,并派发给线程池执行
"""
print("[-] Worker 节点启动,等待接收任务...")
# 初始化线程池,根据服务器配置调整 max_workers(例如 10 到 50)
with ThreadPoolExecutor(max_workers=5) as executor:
while True:
# blpop: 阻塞式弹出。如果队列为空,程序会在这里“睡觉”等待,直到有新任务,不会空耗 CPU
task = redis_client.blpop(REDIS_QUEUE_NAME, timeout=0)
if task:
# task 是一个元组,task[0] 是队列名,task[1] 是取出的 URL
url = task[1]
# 将任务提交给线程池异步执行
executor.submit(fetch_data, url)
def seed_master_urls():
"""
模拟 Master 主节点:负责生产数据,将待抓取的 URL 推送到 Redis 队列
"""
print("[-] Master 正在向队列推送任务...")
# 模拟推送 10 个测试任务
for i in range(1, 11):
# 利用 httpbin 测试我们的代理IP是否生效
url = f"https://httpbin.org/ip?task_id={i}"
redis_client.rpush(REDIS_QUEUE_NAME, url)
print("[-] 任务推送完成!")
if __name__ == "__main__":
# -----------------------------------------------------------------
# 注意:在真实的分布式环境中,Master 和 Worker 是运行在不同服务器上的两套代码。
# 这里为了演示,我们放在同一个脚本中顺序执行。
# -----------------------------------------------------------------
# 1. 主节点推入初始 URL 种子
seed_master_urls()
# 2. 启动 Worker 节点(多线程开始疯狂拉取并抓取数据)
worker_process()
技术难点拆解
- 生产者-消费者模型: 这是一个经典的
Producer-Consumer模式。seed_master_urls()是生产者,负责发现并下发URL;worker_process()是消费者。两者通过 Redis 的blpop和rpush完全解耦。你可以随时增加或减少 Worker 服务器的数量,完全不需要改动代码。 - 阻塞式队列 (
blpop): 这是一个非常优雅的细节。传统的pop如果取不到数据会返回空,你需要写一个死循环加time.sleep。而blpop在队列为空时会自动休眠线程,直到 Master 推入新数据,它会立刻被唤醒,最大程度节约了系统资源。 - 隧道代理的双重保险: 为什么有了分布式还要代理IP?因为一台 16 核 32G 的 Worker 服务器,哪怕开 50 个线程,对外的公网出口依然只有一个 IP。一旦并发量上来,目标网站立刻就能识别出这台机器的异常。爬虫代理让每次请求在服务端被重定向到不同的真实 IP,配合线程池的高并发,可以说是“隐形战斗机群”。
结语
当你把单机爬虫重构为 Redis 分发 + 多线程并发 + 动态隧道代理 时,你就跨过了“爬虫新手村”的门槛。这套架构具备极强的横向扩展能力,能为你源源不断地输送高质量的 AI 训练语料。