因multiprocessing子进程内存隔离,本地set去重失效,需用Redis共享存储实现跨进程URL去重,配合BRPOP队列消费、异常重试及pipeline批量写入保障可靠性。

为什么直接用 multiprocessing 启动爬虫进程容易重复抓取?
因为每个子进程独立运行,requests 发起请求前若只靠本地集合(如 set())判重,进程间内存不共享,URL 去重失效。你看到的“重复抓取”,本质是多个进程同时发现并处理同一个未去重的 URL。
解决思路很明确:把去重逻辑提到进程外、共享存储层。Redis 是最常用选择——它天然支持原子操作、多客户端并发访问,且 SET 或 PFADD 可保证 URL 仅存一份。
- 用
redis.Redis().sadd("urls_seen", url)插入 URL,返回1表示新增、0表示已存在 - 务必搭配
decode_responses=True初始化 Redis 客户端,避免返回字节串导致后续字符串比较出错 - 不要在子进程中新建 Redis 连接池——每个进程都建连接会快速耗尽 Redis 的
maxclients限制;应复用连接或使用连接池(如redis.ConnectionPool)
如何让每个 multiprocessing.Process 安全消费待抓取队列?
别用全局变量或文件模拟队列——竞态条件高发。Redis 的 LPUSH/RPOP 或更稳妥的 BRPOP(阻塞式弹出)才是生产级做法。
典型流程:主进程预加载一批种子 URL 到 Redis List(如键名 url_queue),各子进程循环执行 redis_client.brpop("url_queue", timeout=2)。超时返回 None,可视为任务结束信号。
立即学习“Python免费学习笔记(深入)”;
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
-
brpop是原子操作,多个进程争抢同一个 URL 不会重复弹出 - 若中途某进程崩溃,未处理完的 URL 不会丢失——只要没
DEL或ACK就还在队列里,其他进程能继续消费 - 避免用
rpop+if判空组合,这中间存在竞态窗口:两个进程同时读到非空,接着都执行rpop,第二个会拿到None但可能误判为正常结束
redis-py 连接异常时为什么子进程直接挂掉?
默认情况下,redis.Redis 在网络断开或超时时抛出 ConnectionError、TimeoutError 等异常,而子进程未捕获就终止,导致整个爬虫提前退出。
必须在每个子进程的主循环里包一层异常处理,并设计重试退避逻辑:
- 捕获
redis.exceptions.ConnectionError和redis.exceptions.TimeoutError - 重试前加
time.sleep(1 * (2 ** retry_count))(指数退避),避免雪崩式重连 - 设置最大重试次数(如 3 次),超过则记录日志并
break退出该进程,让主进程监控并重启或忽略 - 注意:Redis 连接对象本身不是线程/进程安全的,但每个子进程持有一个独立实例就没问题——别跨进程传递
redis.Redis实例
抓取结果怎么存进 Redis 又不拖慢爬虫速度?
别在每次抓取后同步写 Redis(如 redis_client.hset("results", url, content))。高频写入会成为瓶颈,尤其当网络延迟波动时。
更合理的做法是:子进程本地缓存一批结果(比如每 10 条或每 2 秒),再批量写入 Redis —— 用 redis_client.pipeline() 提交:
pipe = redis_client.pipeline()
for url, data in batch:
pipe.hset("results", url, data)
pipe.execute()
-
pipeline减少网络往返次数,吞吐量通常提升 3–5 倍 - 注意管道命令数不宜过多(如单次超 1000 条),否则 Redis 单次响应过大可能触发 client-output-buffer-limit 限制
- 如果结果需要持久化或后续分析,建议只存关键字段(如标题、状态码、时间戳),大文本内容存本地文件或对象存储,Redis 里只留路径引用
真正难的不是写几行 multiprocessing 和 redis 代码,而是想清楚每个环节谁负责判重、谁负责分发、谁兜底失败——这些边界一旦模糊,加再多进程反而放大问题。

















