> 文章列表 > APScheduler定时任务

APScheduler定时任务

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触发器: 想要在固定的时间间隔触发事件。

  1. weeks: 运行任务的周数间隔(默认为0),整型
  2. days: 运行任务的天数间隔(默认为0), 整型
  3. hours: 运行任务的小时间隔(默认为0),整型
  4. minutes: 运行任务的分钟间隔(默认为0),整型
  5. seconds: 运行任务的秒数间隔(默认为0),整型
  6. start_date: 任务首次运行的时间
  7. end_date: 任务结束的时间, 执行任务的时间浮动范围
  8. 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触发器: 在某个确切的时间周期性的触发事件。

  1. year:4 位数字,取值范围为 0000~9999,表示任务的运行年份。
  2. month:1~12 的数字或指定的缩写(比如 JAN 表示一月),表示任务的运行月份。
  3. day:1~31 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行日期。
  4. week:1~7 的数字或指定的缩写(比如 SUN 表示周日),表示任务的运行星期(与 day 参数二选一)。
  5. day_of_week:0~6 的数字或指定的缩写(mon、 tue、 wed、 thu、 fri、 sat、 sun),表示任务的运行星期几(与 day 参数二选一)。注意:0 和 7 都表示周日。
  6. hour: 0~23 的数字,表示任务的运行小时数。
  7. minute: 0~59 的数字,表示任务的运行分钟数。
  8. second: 0~59 的数字,表示任务的运行秒数。
  9. start_date: datetime类型或者字符串类型,起始时间。
  10. end_date: datetime类型或者字符串类型,结束时间。
  11. timezone:时区。
  12. 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定时器配置参考 -------------------"""

五、调度器

  1. BlockingScheduler:适用于调度程序是进程中唯一运行的进程,调用 start函数会阻塞当前线程,不能立即返回。
  2. BackgroundScheduler:适用于调度程序在应用程序的后台运行,调用 start后主线程不会阻塞。
  3. AsyncIOScheduler:适用于使用了 asyncio模块的应用程序。
  4. GeventScheduler:适用于使用 gevent模块的应用程序。
  5. TwistedScheduler:适用于构建 Twisted的应用程序。
  6. QtScheduler:适用于构建 Qt的应用程序。

六、任务存储器

任务存储器的选择有两种。
一是内存,也是默认的配置。二是数据库。
使用内存的方式是简单高效,但是不好的是,一旦程序出现问题,重新运行的话,会把之前已经执行了的任务重新执行一遍。
数据库则可以在程序崩溃后,重新运行可以从之前中断的地方恢复正常运行。有以下几种选择:

  1. MemoryJobStore:没有序列化,任务存储在内存中,增删改查都是在内存中完成。
  2. SQLAlchemyJobStore:使用 SQLAlchemy这个 ORM框架作为存储方式。
  3. MongoDBJobStore:使用 mongodb作为存储器。
  4. RedisJobStore:使用 redis作为存储器。

七、 执行器

执行器的选择取决于应用场景。
通常默认的 ThreadPoolExecutor已经在大部分情况下是可以满足我们需求的。
如果我们的任务涉及到一些 CPU密集计算的操作。那么应该考虑 ProcessPoolExecutor。
然后针对每种程序, apscheduler也设置了不同的 executor:

  1. ThreadPoolExecutor:线程池执行器。
  2. ProcessPoolExecutor:进程池执行器。
  3. GeventExecutor: Gevent程序执行器。
  4. TornadoExecutor: Tornado程序执行器。
  5. TwistedExecutor: Twisted程序执行器。
  6. AsyncIOExecutor: asyncio程序执行器。

八、定时任务调度配置

  1. 执行器:
    配置 default执行器为 ThreadPoolExecutor,并且设置最多的线程数是20个。

  2. 存储器:
    配置 default的任务存储器为 SQLAlchemyJobStore(使用SQLite)。

  3. 任务配置:
    设置 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()

九、任务操作

  1. 添加任务:
    使用 scheduler.add_job(job_obj,args,id,trigger,**trigger_kwargs)。

  2. 删除任务:
    使用 scheduler.remove_job(job_id,jobstore=None)。

  3. 暂停任务:
    使用 scheduler.pause_job(job_id,jobstore=None)。

  4. 恢复任务:
    使用 scheduler.resume_job(job_id,jobstore=None)。

  5. 修改某个任务属性信息:
    使用 scheduler.modify_job(job_id,jobstore=None,**changes)。

  6. 修改单个作业的触发器并更新下次运行时间:
    使用 scheduler.reschedule_job(job_id,jobstore=None,trigger=None,**trigger_args)

  7. 输出作业信息:
    使用 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')