Python/Selenium并发爬虫sleep装饰器每5次调用休眠异常排查
问题背景
基于Python/Selenium开发网页爬虫项目时,为避免请求频率过高给目标服务器造成过大压力,自定义了sleepy装饰器,设计逻辑为每完成5次函数调用(即向服务器发起5次请求)后自动执行sleep(20)休眠20秒,项目采用ThreadPoolExecutor线程池实现多任务并发执行。
问题现象
- 终端打印的运行日志中,装饰器的调用计数、
Sleeping...休眠触发提示输出均符合预期 - 观察输出文件
Hitachi.csv的生成过程时发现,程序并未在处理第5个URL时按预期暂停,反而在任务执行末尾才出现停顿 - 初步判断
sleepy装饰器并未在第5次函数调用节点正确执行休眠逻辑
核心问题代码
def sleepy(f): def wrapped(*args, **kwargs): wrapped.calls += 1 print(f"{f.__name__} called {wrapped.calls} times") if wrapped.calls % 5 == 0: print("Sleeping...") sleep(20) return f(*args, **kwargs) wrapped.calls = 0 return wrapped
问题根因
执行顺序不符合设计预期
当前装饰器的执行流为「计数累加→判断是否触发休眠→执行休眠→执行实际爬取函数」,和设计要求的「完成5次请求后再休眠」逻辑完全相反,是先触发休眠再发送对应批次的请求,而非请求发送完成后休眠。
多线程并发下的逻辑缺陷
- 共享计数
wrapped.calls的累加操作不是线程安全的,多线程并发时会出现竞态条件,日志计数显示正常是print操作受GIL影响产生的巧合,计数和实际请求完成数的对应关系是错乱的 sleep(20)只会阻塞当前触发计数阈值的单个工作线程,线程池内其他工作线程完全不受休眠逻辑影响,会继续发起请求,根本无法实现全局请求暂停。观察到的末尾停顿,是任务执行到最后阶段时线程池仅剩触发休眠的单个线程在运行,其余线程已完成任务退出,才会出现明显停顿。
修复方案
- 调整装饰器执行顺序,先执行实际爬取请求,请求完成后再做计数累加和休眠判断
- 引入线程锁保证计数、休眠判断操作的原子性,休眠期间持有锁,阻塞其他完成请求的线程继续推进,实现全局节流
- 如果线程池最大工作线程数大于5,该方案仍可能出现瞬时请求超量,更稳妥的方案是替换为线程安全的令牌桶/漏桶限流器,全局控制单位时间的总请求数
修复后的装饰器参考代码:
import time from threading import Lock def sleepy(f): call_lock = Lock() def wrapped(*args, **kwargs): # 先执行实际爬取逻辑 result = f(*args, **kwargs) # 加锁保证计数和休眠逻辑的线程安全 with call_lock: wrapped.calls += 1 print(f"{f.__name__} called {wrapped.calls} times") if wrapped.calls % 5 == 0: print("Sleeping...") time.sleep(20) return result wrapped.calls = 0 return wrapped
内容的提问来源于stack exchange,提问作者Martin H
相关产品推荐
相关产品推荐

