You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)只会阻塞当前触发计数阈值的单个工作线程,线程池内其他工作线程完全不受休眠逻辑影响,会继续发起请求,根本无法实现全局请求暂停。观察到的末尾停顿,是任务执行到最后阶段时线程池仅剩触发休眠的单个线程在运行,其余线程已完成任务退出,才会出现明显停顿。
修复方案
  1. 调整装饰器执行顺序,先执行实际爬取请求,请求完成后再做计数累加和休眠判断
  2. 引入线程锁保证计数、休眠判断操作的原子性,休眠期间持有锁,阻塞其他完成请求的线程继续推进,实现全局节流
  3. 如果线程池最大工作线程数大于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 02:54:20