APScheduler定时任务
APScheduler定时任务
一、简介
APScheduler使用起来十分方便,提供了基于日期,固定时间间隔以及crontab类型的任务,还可以在程序运行过程中动态的新增任务和删除任务。在任务运行过程中,还可以把任务存储起来,下次启动运行依然保留之前的状态。另外最重要的一个特点是:APScheduler基于Python语言的库,所以是可以跨平台运行的。
如果我们需要在项目中开一个定时功能,完全可以选择 APScheduler,轻量又功能强大。
二、安装
pip install apscheduler
三、四个基本对象
1. 触发器(triggers)
触发器就是根据你指定的触发方式,比如是按照时间间隔,还是按照 crontab触发,触发条件是什么等。每个任务都有自己的触发器。
2. 任务存储器(job stores)
任务存储器是可以存储任务的地方,默认情况下任务保存在内存,也可将任务保存在各种数据库中。任务存储进去后,会进行序列化,然后也可以反序列化提取出来,继续执行。
3. 执行器(executors)
执行器的目的是安排任务到线程池或者进程池中运行的。
4. 调度器(schedulers)
任务调度器是属于整个调度的总指挥官。他会合理安排作业存储器、执行器、触发器进行工作,并进行添加和删除任务等。调度器通常是只有一个的。开发人员很少直接操作触发器、存储器、执行器等。因为这些都由调度器自动来实现了。
四、触发器
1. date触发器: 在某个日期时间只触发一次事件。
date 触发器,其参数如下:
run_date:表示要运行任务的日期和时间,类型为 datetime.datetime 对象。
from apscheduler.schedulers.blocking import BlockingScheduler
import datetimedef do_job(text):print("Hello! ")if __name__ == "__main__":schedule = BlockingScheduler()# 创建一个在 2023 年 5 月 1 日中午 12 点运行的 date 触发器schedule.add_job(do_job, "date", run_date=datetime.datetime(2023, 5, 1, 12, 0), args=["text"])schedule.start()
2. interval触发器: 想要在固定的时间间隔触发事件。
- weeks: 运行任务的周数间隔(默认为0),整型
- days: 运行任务的天数间隔(默认为0), 整型
- hours: 运行任务的小时间隔(默认为0),整型
- minutes: 运行任务的分钟间隔(默认为0),整型
- seconds: 运行任务的秒数间隔(默认为0),整型
- start_date: 任务首次运行的时间
- end_date: 任务结束的时间, 执行任务的时间浮动范围
- jitter: 触发的时间误差
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetimedef do_job():print("Hello! The time is "%datetime.now())if __name__ == "__main__":scheduler = BlockingScheduler()scheduler.add_job(do_job, "interval", seconds=3)scheduler.start()"""# ------------- start: interval定时器配置参考 -------------------# 定义一个定时任务:通过interval的形式来定时启动任务, 每小时执行一次函数@scheduler.service('interval', id='do_job', hours=1)def do_job():print("Hello! The time is "%datetime.now())# 定义一个定时任务:通过interval的形式来定时启动任务, 每10分钟执行一次函数@scheduler.service('interval', id='do_job', minutes=10)def do_job():print("Hello! The time is "%datetime.now())# -------------End: interval定时器配置参考 -------------------"""
3. crontab触发器: 在某个确切的时间周期性的触发事件。
- year:4 位数字,取值范围为 0000~9999,表示任务的运行年份。
- month:1~12 的数字或指定的缩写(比如 JAN 表示一月),表示任务的运行月份。
- day:1~31 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行日期。
- week:1~7 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行星期(与 day 参数二选一)。
- day_of_week:0~6 的数字或指定的缩写(mon、 tue、 wed、 thu、 fri、 sat、 sun),表示任务的运行星期几(与 day 参数二选一)。注意:0 和 7 都表示周日。
- hour: 0~23 的数字,表示任务的运行小时数。
- minute: 0~59 的数字,表示任务的运行分钟数。
- second: 0~59 的数字,表示任务的运行秒数。
- start_date: datetime类型或者字符串类型,起始时间。
- end_date: datetime类型或者字符串类型,结束时间。
- timezone:时区。
- jitter:任务触发的误差时间,执行任务的时间浮动范围。
也可以用表达式类型,可以用以下方式:
表达式 | 字段 | 描述 |
---|---|---|
* | 任何 | 在每个值都触发 |
*/a | 任何 | 每间隔a触发一次 |
a-b | 任何 | 在a-b区间内任何一个时间触发(a必须小于b) |
a-b / c | 任何 | 在a-b区间内每间隔c触发一次 |
xth y | day | 第x个星期y触发 |
last x | day | 最后一个星期x触发 |
last | day | 一个月中的最后一天触发 |
x,y, z | 任何 | 可以把上面的表达式进行组合 |
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetimedef do_job():print("Hello! The time is "%datetime.now())if __name__ == "__main__":scheduler = BlockingScheduler()scheduler.add_job(do_job, "corn", day="4th sun", hour=20, minute=1)scheduler.start()"""# ------------- start: interval定时器配置参考 -------------------# 定义一个定时任务:通过cron的形式来定时启动任务, 每天的xx:xx:xx时刻执行一次函数@scheduler.service('cron', id='do_job', day='*', hour='00', minute='00', second='00')def do_job():print("Hello! The time is "%datetime.now())# -------------End: interval定时器配置参考 -------------------"""
五、调度器
- BlockingScheduler:适用于调度程序是进程中唯一运行的进程,调用 start函数会阻塞当前线程,不能立即返回。
- BackgroundScheduler:适用于调度程序在应用程序的后台运行,调用 start后主线程不会阻塞。
- AsyncIOScheduler:适用于使用了 asyncio模块的应用程序。
- GeventScheduler:适用于使用 gevent模块的应用程序。
- TwistedScheduler:适用于构建 Twisted的应用程序。
- QtScheduler:适用于构建 Qt的应用程序。
六、任务存储器
任务存储器的选择有两种。
一是内存,也是默认的配置。二是数据库。
使用内存的方式是简单高效,但是不好的是,一旦程序出现问题,重新运行的话,会把之前已经执行了的任务重新执行一遍。
数据库则可以在程序崩溃后,重新运行可以从之前中断的地方恢复正常运行。有以下几种选择:
- MemoryJobStore:没有序列化,任务存储在内存中,增删改查都是在内存中完成。
- SQLAlchemyJobStore:使用 SQLAlchemy这个 ORM框架作为存储方式。
- MongoDBJobStore:使用 mongodb作为存储器。
- RedisJobStore:使用 redis作为存储器。
七、 执行器
执行器的选择取决于应用场景。
通常默认的 ThreadPoolExecutor已经在大部分情况下是可以满足我们需求的。
如果我们的任务涉及到一些 CPU密集计算的操作。那么应该考虑 ProcessPoolExecutor。
然后针对每种程序, apscheduler也设置了不同的 executor:
- ThreadPoolExecutor:线程池执行器。
- ProcessPoolExecutor:进程池执行器。
- GeventExecutor: Gevent程序执行器。
- TornadoExecutor: Tornado程序执行器。
- TwistedExecutor: Twisted程序执行器。
- AsyncIOExecutor: asyncio程序执行器。
八、定时任务调度配置
-
执行器:
配置 default执行器为 ThreadPoolExecutor,并且设置最多的线程数是20个。 -
存储器:
配置 default的任务存储器为 SQLAlchemyJobStore(使用SQLite)。 -
任务配置:
设置 coalesce为 False:设置这个目的是,比如由于某个原因导致某个任务积攒了很多次没有执行(比如有一个任务是1分钟跑一次,但是系统原因断了5分钟),如果 coalesce=True,那么下次恢复运行的时候,会只执行一次,而如果设置 coalesce=False,那么就不会合并,会5次全部执行。
max_instances=5:同一个任务同一时间最多只能有5个实例在运行。比如一个耗时10分钟的job,被指定每分钟运行1次,如果我 max_instance值5,那么在第6~10分钟上,新的运行实例不会被执行,因为已经有5个实例在跑了。
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor
from datetime import datetimejobstores = {"default": SQLAlchemyJobStore(url="sqlite:///jobs.sqlite")
}
executors = {"default": ThreadPoolExecutor(20)
}
job_defaults = {"coalesce": False,"max_instances": 3
}def do_job():print("Hello! The time is "%datetime.now())if __name__ == "__main__":schedule = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)schedule.add_job(do_job, "interval", seconds=30)schedule.start()
九、任务操作
-
添加任务:
使用 scheduler.add_job(job_obj,args,id,trigger,**trigger_kwargs)。 -
删除任务:
使用 scheduler.remove_job(job_id,jobstore=None)。 -
暂停任务:
使用 scheduler.pause_job(job_id,jobstore=None)。 -
恢复任务:
使用 scheduler.resume_job(job_id,jobstore=None)。 -
修改某个任务属性信息:
使用 scheduler.modify_job(job_id,jobstore=None,**changes)。 -
修改单个作业的触发器并更新下次运行时间:
使用 scheduler.reschedule_job(job_id,jobstore=None,trigger=None,**trigger_args) -
输出作业信息:
使用 scheduler.print_jobs(jobstore=None,out=sys.stdout)
十、异常监听
当我们的任务抛出异常后,我们可以监听到,然后把错误信息进行记录。示例代码如下:
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
import datetime
import logging# 配置日志显示
logging.basicConfig(level=logging.INFO,format="%(asctime)s %(filename)s[line%(lineno)d] %(levelname)s %()",datefmt="%Y-%m-%d %H:%M:%S",filename="log1.txt",filemode="a")def do_job(x):print("Hello! The time is "%datetime.datetime.now(), x)# 故意抛出异常,因为除数不能为0print(1/0)def do_job2(x):print("Hello! The time is "%datetime.datetime.now(), x)def my_listener(event):if event.exception:print("任务出错了")else:print("任务正常运行...")if __name__ == "__main__":schedule = BlockingScheduler()schedule.add_job(do_job, args=("一次性任务会出错", ), next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=15), id="do_job")schedule.add_job(do_job2, args=("循环任务", ), trigger="interval", seconds=3, id="do_job2")# 配置任务执行完成和执行错误的监听schedule.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)# 设置日志schedule._logger = loggingschedule.start()
十一、对触发器某些参数的理解
1. 怎么理解jitter?
在APScheduler模块的定时配置表达式中,jitter是一个参数,指定启用摆动的最大秒数。这个参数允许我们添加一些随机性,以防止定时任务对系统和服务产生过大的负荷。
当jitter大于0时,调度器在使用指定的触发器计算下一个任务运行时间后,会在下一次任务运行时间附近添加一个随机值(最多摆动jitter秒)。这样可以确保在多个任务同时启动时,它们不会同时运行或者同时停运。
举个例子,如果我们定义了一个每10分钟执行一次的定时任务,并将jitter设置为60秒,那么实际上这个任务在10分钟后运行的时间点将在10分钟后的随机时间加上60秒内的随机数之间。
这种方法可以防止系统过度负载,尤其在大量定时任务需要同时运行的情况下特别有用。
2. start_date和end_date必须同时使用吗?
start_date和end_date这两个参数不必须同时使用,但它们一般都是配合使用的。
start_date是一个可选参数,指定定时器的开始时间。如果未指定此参数,则定时器会从当前时间开始计算。end_date也是一个可选参数,指定定时器的结束时间,即定时器应该停止工作的时刻。如果未指定end_date参数,则定时器将一直保持工作状态,直到被显式停止。
如果您想让定时器在一个确定的时间段内工作,您可以使用start_date和end_date两个参数。以每天上午9点到下午5点之间运行的定时任务为例,可以这样配置:
python
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetime, timedef job_func():print('Job running at', datetime.now())# 创建一个 BlockingScheduler 实例
scheduler = BlockingScheduler()# 定义一个每分钟运行一次的任务
scheduler.add_job(job_func, 'interval', minutes=1, # 使用了 datetime.combine() 方法来生成每天的上午 9 点和下午 5 点的时间start_date=datetime.combine(datetime.today(), time(9, 0)), # 今天的上午九点end_date=datetime.combine(datetime.today(), time(17, 0)) # 今天的下午五点
)# 启动调度器
scheduler.start()
3. 触发器(interval)各个参数优先级问题
根据APScheduler文档[1],当设置 interval 类型的触发器时,有以下八个参数可用于控制任务执行的时间和频率:
weeks:间隔的周数
days:间隔的天数
hours:间隔的小时数
minutes:间隔的分钟数
seconds:间隔的秒数
start_date:任务首次运行的时间
end_date:任务结束的时间
jitter:执行任务的时间浮动范围
当使用 interval 触发器时,这些参数的优先级是根据它们在参数列表中的顺序而定。因此,前面的参数的优先级要高于后面的参数,如果没有指定某个参数,则默认为0。
例如,如果同时指定了 days 和 hours 参数,则任务将以每 days 天 hours 小时为间隔运行。如果同时指定了 start_date 和 days 参数,则任务将在 start_date 指定的时间开始,每 days 天运行一次。
需要注意的是,在一些情况下,jitter 参数可能会影响其它参数的优先级。例如,如果将 jitter 设置为5秒,并且 seconds 参数设置为60秒,则实际执行任务的时间可能会在55到65秒之间浮动。
综上所述,这些参数的优先级主要依赖于它们的位置,前面的参数的优先级要高于后面的参数。同时,jitter 参数可能会影响其它参数的优先级。
# 每隔两分钟执行一次 job_func 方法
scheduler .add_job(job_func, 'interval', minutes=2)# 在 2017-12-13 14:00:01 ~ 2017-12-13 14:00:10 之间, 每隔两分钟执行一次 job_func 方法
scheduler .add_job(job_func, 'interval', minutes=2, start_date='2017-12-13 14:00:01' , end_date='2017-12-13 14:00:10')
3. 触发器(corn)各个参数优先级问题
对于 corn 触发器,各个参数的优先级如下:
year(年份):4 位数字,取值范围为 0000~9999,表示任务的运行年份。
month(月份):1~12 的数字或指定的缩写(比如 JAN 表示一月),表示任务的运行月份。
day(日期):1~31 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行日期。
week(星期):1~7 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行星期(与 day 参数二选一)。
day_of_week(星期几):0~6 的数字或指定的缩写(比如 SUN 表示周日,0 和 7 都表示周日),表示任务的运行星期几(与 day 参数二选一)。
hour(小时):0~23 的数字,表示任务的运行小时数。
minute(分钟):0~59 的数字,表示任务的运行分钟数。
second(秒钟):0~59 的数字,表示任务的运行秒数。
需要注意的是,这些参数的优先级是按照它们在参数列表中的顺序而定。也就是说,如果在两个参数中同时指定了一个时间,则优先级小的会被忽略。例如,如果指定了 day 和 day_of_week 参数,则 day_of_week 参数会被忽略。
当指定了多个参数时,corn 触发器会按照顺序依次匹配每个参数,并将匹配到的时间值传递给下一个参数。因此,匹配顺序非常重要,应该根据实际需求来指定参数的顺序。
# 在每年 1-3、7-9 月份中的每个星期一、二中的 00:00, 01:00, 02:00 和 03:00 执行 job_func 任务
scheduler .add_job(job_func, 'cron', month='1-3,7-9',day='0, tue', hour='0-3')