apscheduler仅负责定时触发,爬虫逻辑需独立封装并确保线程安全、可重入及异常处理;应避免直接传lambda调用requests,须用crontrigger或intervaltrigger合理调度,设置max_instances=1防并发,持久化存储防重启丢失。

APScheduler 本身不负责爬取,只负责定时触发;真正干活的是你写的爬虫函数,它必须是线程安全、可重入、能处理网络异常的。
为什么 job_func 不能直接写 requests.get() 调用
常见错误是把爬虫逻辑硬塞进 add_job() 的函数参数里,比如传一个 lambda: requests.get(url) —— 这会导致每次调度都新建请求但无错误捕获、无重试、无状态隔离。更糟的是,若该函数抛出未捕获异常,APScheduler 默认会静默丢弃该 job(取决于 misfire_grace_time 和 coalesce 设置)。
正确做法是封装成独立函数,并显式处理关键路径:
- 用
try/except包住整个请求+解析逻辑,至少捕获requests.RequestException和TimeoutError - 避免在 job 函数里操作全局变量或共享列表,除非加锁;推荐每次调用都新建 session 或使用
threading.local() - 如果依赖登录态(如 Cookie),不要复用同一个
requests.Session()实例跨 job 调用,session 不是线程安全的
选择合适的 trigger:interval vs cron
IntervalTrigger 看似简单,但实际容易误用。例如设 minutes=5,不代表“每小时第 0、5、10… 分执行”,而是“从首次运行起,每隔 5 分钟触发一次”,可能漂移。而 CronTrigger 支持精确到分钟级的调度(如 minute='*/5'),更适合对齐自然时间窗口的爬取任务。
实操建议:
- 需要固定时刻执行(如每天 9:00 抓日报)→ 用
CronTrigger(hour=9, minute=0) - 只需大致间隔(如“大约每 30 秒查一次接口状态”)→ 用
IntervalTrigger(seconds=30) - 避免混用:不要在
IntervalTrigger中设start_date为过去时间,否则可能批量补跑
如何防止 job 堆积和并发冲突
默认情况下,APScheduler 允许 job 并发执行。如果一个爬虫任务耗时 8 秒,而 interval 是 5 秒,第三次调度进来时,前两次可能还在跑 —— 导致重复请求、IP 被限、数据错乱。
必须显式控制并发行为:
- 加
max_instances=1(最常用):同一 job 最多只允许一个实例运行,后续触发会被丢弃或等待(取决于coalesce) - 设
coalesce=True:当多个调度点错过时,只执行最后一次(适合“状态同步”类任务) - 设
misfire_grace_time=30:允许最多延迟 30 秒执行,超时则跳过(防止雪崩) - 慎用
replace_existing=True:仅在热更新 job 时需要,日常启动不建议依赖它
持久化 job 并避免重启丢失
默认内存存储(MemoryJobStore)下,程序一重启,所有定时任务就消失。生产环境必须换存储后端。
推荐组合:
- 轻量级:用
SQLAlchemyJobStore配 SQLite(单机够用),注意设置url='sqlite:///jobs.sqlite'并提前建表(APScheduler 会自动初始化) - 分布式:换
RedisJobStore,需装redis包,配置host/port即可,天然支持多实例共享 job 状态 - 切记:改用持久化存储后,
add_job()的 job_id 必须唯一且稳定(别用随机字符串),否则每次启动都算新 job
真正的难点不在怎么加定时,而在于让每次触发都可靠地完成一次完整爬取闭环:请求、解析、去重、存库、异常降级。APScheduler 只是那个准时敲钟的人,钟声响了,你得确保屋里有人醒着、有电、有网、有备用方案。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











