APScheduler
APScheduler全称为Advanced Python Scheduler,是一款轻量级的Python任务调度框架。它允许你像Cron那样安排定期执行的任务,并且支持Python函数或任意可调用的对象。官方文档:https://apscheduler.readthedocs.io/en/latest/userguide.html#basic-concepts
1. 安装
pip install apscheduler
2. 组件
triggers(触发器):
触发器中包含调度逻辑,描述一个任务何时被触发,按日期或按时间间隔或按 cronjob 表达式三种方式触发,每个作业都由自己的触发器来决定下次运行时间。除了他们自己初始配置意外,触发器完全是无状态的。
当你调度作业的时候,你需要为这个作业选择一个触发器,用来描述这个作业何时被触发,APScheduler有三种内置的触发器类型:
-
date 一次性指定日期
最基本的一种调度,作业只会执行一次。它的参数如下:run_date (datetime|str) – 作业的运行日期或时间
timezone (datetime.tzinfo|str) – 指定时区例:
# 2016-12-12运行一次job_function sched.add_job(job_function, 'date', run_date=date(2016, 12, 12), args=['text']) # 2016-12-12 12:00:00运行一次job_function sched.add_job(job_function, 'date', run_date=datetime(2016, 12, 12, 12, 0, 0), args=['text'])
-
interval 间隔调度
间隔调度,在某个时间范围内间隔多长时间执行一次 。参数如下:
weeks (int) – 间隔几周
days (int) – 间隔几天
hours (int) – 间隔几小时
minutes (int) – 间隔几分钟
seconds (int) – 间隔多少秒
start_date (datetime|str) – 开始日期
end_date (datetime|str) – 结束日期
timezone (datetime.tzinfo|str) – 时区例:
# 每两个小时调一下job_function sched.add_job(job_function, 'interval', hours=2)
-
cron 和Linux crontab格式兼容,最为强大
参数如下:
year (int|str) – 年,4位数字
month (int|str) – 月 (范围1-12)
day (int|str) – 日 (范围1-31)
week (int|str) – 周 (范围1-53)
day_of_week (int|str) – 周内第几天或者星期几 (范围0-6 或者 mon,tue,wed,thu,fri,sat,sun)
hour (int|str) – 时 (范围0-23)
minute (int|str) – 分 (范围0-59)
second (int|str) – 秒 (范围0-59)
start_date (datetime|str) – 最早开始日期(包含)
end_date (datetime|str) – 最晚结束时间(包含)
timezone (datetime.tzinfo|str) – 指定时区例:
# job_function将会在6,7,8,11,12月的第3个周五的1,2,3点运行 sched.add_job(job_function, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3') # 截止到2016-12-30 00:00:00,每周一到周五早上五点半运行job_function sched.add_job(job_function, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2016-12-31')
job stores(作业存储器):
作业存储器的选择有两种:一是内存,也是默认的配置;二是数据库。具体选哪一种看我们的应用程序在崩溃时是否重启整个应用程序,如果重启整个应用程序,那么作业会被重新添加到调度器中,此时简单的选取内存作为作业存储器即简单又高效。但是,当调度器重启或应用程序崩溃时您需要您的作业从中断时恢复正常运行,那么通常我们选择将作业存储在数据库中,使用哪种数据库通常取决于为在您的编程环境中使用了什么数据库。我们可以自由选择,PostgreSQL 是推荐的选择,因为它具有强大的数据完整性保护。
存储被调度的作业,默认的作业存储器只是简单地把作业保存在内存中,其他的作业存储器则是将作业保存在数据库中。当作业被保存到一个持久化的作业存储器中的时候,该作业的数据会被序列化,并在加载时被反序列化。作业存储器充当保存、加载、更新和查找作业的中间商。在调度器之间不能共享作业存储。
如果你的应用在每次启动的时候都会重新创建作业,那么使用默认的作业存储器(MemoryJobStore)即可,但是如果你需要在调度器重启或者应用程序奔溃的情况下任然保留作业,你应该根据你的应用环境来选择具体的作业存储器。例如:使用Mongo或者SQLAlchemy JobStore (用于支持大多数RDBMS)
executors(执行器):
执行器是将指定的作业(调用函数)提交到线程池或进程池中运行,当任务完成时,执行器通知调度器触发相应的事件。
对执行器的选择取决于你使用上面哪些框架,大多数情况下,使用默认的ThreadPoolExecutor已经能够满足需求。如果你的应用涉及到CPU密集型操作,你可以考虑使用ProcessPoolExecutor来使用更多的CPU核心。你也可以同时使用两者,将ProcessPoolExecutor作为第二执行器。
schedulers(调度器):
任务调度器,属于控制角色,通过它配置作业存储器、执行器和触发器,添加、修改和删除任务。调度器协调触发器、作业存储器、执行器的运行,通常只有一个调度程序运行在应用程序中,开发人员通常不需要直接处理作业存储器、执行器或触发器,配置作业存储器和执行器是通过调度器来完成的。
调度器的主循环其实就是反复检查是不是有到时需要执行的任务,分以下几步进行:
- 1.询问自己的每一个作业存储器,有没有到期需要执行的任务,如果有,计算这些作业中每个作业需要运行的时间点,如果时间点有多个,做 coalesce 检查。
- 2.提交给执行器按时间点运行。
根据不同的应用场景可以选用不同的调度器,可选的有下面 7种。
BlockingScheduler : 当调度器是你应用中唯一要运行的东西时。 调用start函数会阻塞当前线程,不能立即返回。
BackgroundScheduler : 当你没有运行任何其他框架并希望调度器在你应用的后台执行时使用(充电桩即使用此种方式)。 调用start后主线程不会阻塞。
AsyncIOScheduler : 当你的程序使用了asyncio(一个异步框架)的时候使用。
GeventScheduler : 当你的程序使用了gevent(高性能的Python并发框架)的时候使用。
TornadoScheduler : 当你的程序基于Tornado(一个web框架)的时候使用。
TwistedScheduler : 当你的程序使用了Twisted(一个异步框架)的时候使用
QtScheduler : 如果你的应用是一个Qt应用的时候可以使用。
3. 配置调度程序
apscheduler 提供了许多不同的方法来配置调度器。可以使用字典,也可以使用关键字参数传递。首先实例化调度程序,添加作业,然后配置调度器,获得最大的灵活性。
在应用程序中使用默认作业存储和默认执行程序运行BackgroundScheduler的例子:
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
这将生成一个名为“default”的MemoryJobStore和名为“default”的ThreadPoolExecutor的BackgroundScheduler,默认最大线程数为10。
如果不满足于当前配置,如希望使用两个执行器有两个作业存储器,并且还想要调整新作业的默认值并设置不同的时区,可按如下配置:
- 配置名为“mongo”的MongoDBJobStore作业存储器
- 配置名为“default”的SQLAlchemyJobStore(使用SQLite)
- 配置名为“default”的ThreadPoolExecutor,最大线程数为20
- 配置名为“processpool”的ProcessPoolExecutor,最大进程数为5
- UTC作为调度器的时区
- coalesce默认情况下关闭
- 作业的默认最大运行实例限制为3
方法一
from pytz import utc
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.mongodb import MongoDBJobStore
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
# 配置作业存储器
jobstores = {
'mongo': MongoDBJobStore(),
'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
# 配置执行器,并设置线程数
executors = {
'default': ThreadPoolExecutor(20),
'processpool': ProcessPoolExecutor(5)
}
job_defaults = {
'coalesce': False, # 默认情况下关闭新的作业
'max_instances': 3 # 设置调度程序将同时运行的特定作业的最大实例数3
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
方法二
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler({
'apscheduler.jobstores.mongo': {
'type': 'mongodb'
},
'apscheduler.jobstores.default': {
'type': 'sqlalchemy',
'url': 'sqlite:///jobs.sqlite'
},
'apscheduler.executors.default': {
'class': 'apscheduler.executors.pool:ThreadPoolExecutor',
'max_workers': '20'
},
'apscheduler.executors.processpool': {
'type': 'processpool',
'max_workers': '5'
},
'apscheduler.job_defaults.coalesce': 'false',
'apscheduler.job_defaults.max_instances': '3',
'apscheduler.timezone': 'UTC',
})
方法三
from pytz import utc
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ProcessPoolExecutor
jobstores = {
'mongo': {'type': 'mongodb'},
'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
executors = {
'default': {'type': 'threadpool', 'max_workers': 20},
'processpool': ProcessPoolExecutor(max_workers=5)
}
job_defaults = {
'coalesce': False,
'max_instances': 3
}
scheduler = BackgroundScheduler()
4. 启动调度器
启动调度器前需要先添加作业,有两种方法向调度器添加作业:一是通过接口add_job(),二是通过使用函数装饰器,其中 add_job() 返回一个apscheduler.job.Job类的实例,用于后续修改或删除作业。
我们可以随时在调度器上调度作业。如果在添加作业时,调度器还没有启动,那么任务将不会运行,并且第一次运行时间在调度器启动时计算。
注意:如果使用的是序列化作业的执行器或作业存储器,那么要求被调用的作业(函数)必须是全局可访问的,被调用的作业的参数是可序列化的,作业存储器中,只有 MemoryJobStore 不会序列化作业。执行器中,只有ProcessPoolExecutor 将序列化作业。
启用调度器只需要调用调度器的 start() 方法,下面分别使用不同的作业存储器来举例说明:
方法一:使用默认的作业存储器:
#coding:utf-8
from apscheduler.schedulers.blocking import BlockingScheduler
import datetime
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
def my_job(id='my_job'):
print (id,'-->',datetime.datetime.now())
jobstores = {
'default': MemoryJobStore()
}
executors = {
'default': ThreadPoolExecutor(20),
'processpool': ProcessPoolExecutor(10)
}
job_defaults = {
'coalesce': False,
'max_instances': 3
}
scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)
scheduler.add_job(my_job, args=['job_interval',],id='job_interval',trigger='interval', seconds=5,replace_existing=True)
scheduler.add_job(my_job, args=['job_cron',],id='job_cron',trigger='cron',month='4-8,11-12',hour='7-11', second='*/10',\
end_date='2018-05-30')
scheduler.add_job(my_job, args=['job_once_now',],id='job_once_now')
scheduler.add_job(my_job, args=['job_date_once',],id='job_date_once',trigger='date',run_date='2018-04-05 07:48:05')
try:
scheduler.start()
except SystemExit:
print('exit')
exit()
结果:
job_once_now --> 2018-04-05 07:48:00.967391
job_date_once --> 2018-04-05 07:48:05.005532
job_interval --> 2018-04-05 07:48:05.954023
job_cron --> 2018-04-05 07:48:10.004431
job_interval --> 2018-04-05 07:48:10.942542
job_interval --> 2018-04-05 07:48:15.952208
job_cron --> 2018-04-05 07:48:20.007123
job_interval --> 2018-04-05 07:48:20.952202
……
上述代码使用内存作为作业存储器,操作比较简单,重启程序相当于第一次运行。
方法二:使用数据库作为存储器:
1 #coding:utf-8
2 from apscheduler.schedulers.blocking import BlockingScheduler
3 import datetime
4 from apscheduler.jobstores.memory import MemoryJobStore
5 from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
6 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
7 def my_job(id='my_job'):
8 print (id,'-->',datetime.datetime.now())
9 jobstores = {
10 'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
11 }
12 executors = {
13 'default': ThreadPoolExecutor(20),
14 'processpool': ProcessPoolExecutor(10)
15 }
16 job_defaults = {
17 'coalesce': False,
18 'max_instances': 3
19 }
20 scheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)
21 scheduler.add_job(my_job, args=['job_interval',],id='job_interval',trigger='interval', seconds=5,replace_existing=True)
22 scheduler.add_job(my_job, args=['job_cron',],id='job_cron',trigger='cron',month='4-8,11-12',hour='7-11', second='*/10',\
23 end_date='2018-05-30')
24 scheduler.add_job(my_job, args=['job_once_now',],id='job_once_now')
25 scheduler.add_job(my_job, args=['job_date_once',],id='job_date_once',trigger='date',run_date='2018-04-05 07:48:05')
26 try:
27 scheduler.start()
28 except SystemExit:
29 print('exit')
30 exit()
结果:
Run time of job "my_job (trigger: date[2018-04-05 07:48:05 CST], next run at: 2018-04-05 07:48:05 CST)" was missed by 0:18:28.898146
job_once_now --> 2018-04-05 08:06:34.010194
job_interval --> 2018-04-05 08:06:38.445843
job_cron --> 2018-04-05 08:06:40.154978
job_interval --> 2018-04-05 08:06:43.285941
job_interval --> 2018-04-05 08:06:48.334360
job_cron --> 2018-04-05 08:06:50.172968
job_interval --> 2018-04-05 08:06:53.281743
job_interval --> 2018-04-05 08:06:58.309952
提示我们有作业本应在 2018-04-05 07:48:05 运行的作业没有运行,因为现在的时间为 2018-04-05 08:06:34,错过了 0:18:28 的时间。
如果将上术代码第 21-25 行注释掉,重新运行本程序,作业仍会运行,说明作业被添加到数据库中,程序中断后重新运行时会自动从数据库读取作业信息,而不需要重新再添加到调度器中,如果不注释 21-25 行添加作业的代码,则作业会重新添加到数据库中,这样就有了两个同样的作业,避免出现这种情况可以在 add_job 的参数中增加** replace_existing=True**,如
scheduler.add_job(my_job, args=['job_interval',],id='job_interval',trigger='interval',seconds=3,replace_existing=True)
如果我们想运行错过运行的作业,使用 misfire_grace_time,如
scheduler.add_job(my_job,args = ['job_cron',] ,id='job_cron',trigger='cron',month='4-8,11-12',hour='7-11',second='*/15',coalesce=True,misfire_grace_time=30,replace_existing=True,end_date='2018-05-30')
说明:misfire_grace_time,假如一个作业本来 08:00 有一次执行,但是由于某种原因没有被调度上,现在 08:01 了,这个 08:00 的运行实例被提交时,会检查它预订运行的时间和当下时间的差值(这里是1分钟),大于我们设置的 30 秒限制,那么这个运行实例不会被执行。
最常见的情形是 scheduler 被 shutdown 后重启,某个任务会积攒了好几次没执行如 5 次,下次这个作业被提交给执行器时,执行 5 次。设置 coalesce=True 后,只会执行一次。
其他操作如下:
1 scheduler.remove_job(job_id,jobstore=None)#删除作业
2 scheduler.remove_all_jobs(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)#输出作业信息
5. 操作作业
-
添加作业
方法一:调用add_job()方法
最常见的方法,add_job()方法返回一个apscheduler.job.Job实例,您可以稍后使用它来修改或删除该作业。
方法二:使用装饰器scheduled_job()
此方法主要是方便的声明在应用程序运行时不会改变的作业
-
删除作业
方法一:通过作业ID或别名调用remove_job()删除作业
方法二:通过add_job()返回的job实例调用remove()方法删除作业
# 实例删除 job = scheduler.add_job(myfunc, 'interval', minutes=2) job.remove() # id删除 scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id') scheduler.remove_job('my_job_id')
-
暂停和恢复作业
可以通过Job实例或调度程序本身轻松暂停和恢复作业。 当作业暂停时,下一个运行时间将被清除,直到作业恢复,不会再计算运行时间。 要暂停作业,请使用以下任一方法:
apscheduler.job.Job.pause() apscheduler.schedulers.base.BaseScheduler.pause_job()
恢复作业:
apscheduler.job.Job.resume() apscheduler.schedulers.base.BaseScheduler.resume_job()
-
获取作业列表
要获得计划作业的机器可处理列表,可以使用get_jobs()方法。 它将返回一个Job实例列表。 如果您只对特定作业存储中包含的作业感兴趣,则将作业存储别名作为第二个参数。
为了方便起见,可以使用print_jobs()方法,它将打印格式化的作业列表,触发器和下次运行时间。
-
修改作业属性
您可以通过调用apscheduler.job.Job.modify()或modify_job()来修改除id以外的任何作业属性。
job.modify(max_instances=6, name='Alternate name')
6. 关闭调度程序
默认情况下,调度程序关闭其作业存储和执行程序,并等待所有当前正在执行的作业完成,wait=False参数可选,代表立即停止,不用等待。
scheduler.shutdown(wait=False)
7. 小例子
定时任务运行脚本小例子:
import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
from app.untils.log_builder import sys_logging
scheduler = BlockingScheduler() # 后台运行
# 设置为每日凌晨00:30:30时执行一次调度程序
@scheduler.scheduled_job("cron", day_of_week='*', hour='1', minute='30', second='30')
def rebate():
print "schedule execute"
sys_logging.debug("statistic scheduler execute success" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
if __name__ == '__main__':
try:
scheduler.start()
sys_logging.debug("statistic scheduler start success")
except (KeyboardInterrupt, SystemExit):
scheduler.shutdown()
sys_logging.debug("statistic scheduler start-up fail")
8. 调度器事件监听
scheduler 的基本应用,在前面已经介绍过了,但仔细思考一下:如果程序有异常抛出会影响整个调度任务吗?请看下面的代码,运行一下看看会发生什么情况:
1 # coding:utf-8
2 from apscheduler.schedulers.blocking import BlockingScheduler
3 import datetime
4
5 def aps_test(x):
6 print (1/0)
7 print (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)
8
9 scheduler = BlockingScheduler()
10 scheduler.add_job(func=aps_test, args=('定时任务',), trigger='cron', second='*/5')
11
12 scheduler.start()
运行结果:
Job "aps_test (trigger: cron[second='*/5'], next run at: 2018-04-05 12:46:35 CST)" raised an exception
Traceback (most recent call last):
File "C:\Users\xx\AppData\Local\Programs\python\python36\lib\site-packages\apscheduler\executors\base.py", line 125, in run_job
retval = job.func(*job.args, **job.kwargs)
File "C:/Users/xx/PycharmProjects/mysite/staff/test2.py", line 7, in aps_test
print (1/0)
ZeroDivisionError: division by zero
Job "aps_test (trigger: cron[second='*/5'], next run at: 2018-04-05 12:46:35 CST)" raised an exception
Traceback (most recent call last):
File "C:\Users\xx\AppData\Local\Programs\python\python36\lib\site-packages\apscheduler\executors\base.py", line 125, in run_job
retval = job.func(*job.args, **job.kwargs)
File "C:/Users/xx/PycharmProjects/mysite/staff/test2.py", line 7, in aps_test
print (1/0)
ZeroDivisionError: division by zero
可能看出每 5 秒抛出一次报错信息。任何代码都可能抛出异常,关键是,发生导常事件,如何第一时间知道,这才是我们最关心的,apscheduler 已经为我们想到了这些,提供了事件监听来解决这一问题。
将上述代码稍做调整,加入日志记录和事件监听,如下所示。
1 # coding:utf-8
2 from apscheduler.schedulers.blocking import BlockingScheduler
3 from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
4 import datetime
5 import logging
6 # 配置日志记录信息,日志文件在当前路径,文件名为 “log1.txt”
7 logging.basicConfig(level=logging.INFO,
8 format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
9 datefmt='%Y-%m-%d %H:%M:%S',
10 filename='log1.txt',
11 filemode='a')
12
13
14 def aps_test(x):
15 print (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)
16
17
18 def date_test(x):
19 print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)
20 print (1/0)
21
22 # 定义一个事件监听,出现意外情况打印相关信息报警。
23 def my_listener(event):
24 if event.exception:
25 print ('任务出错了!!!!!!')
26 else:
27 print ('任务照常运行...')
28
29 scheduler = BlockingScheduler()
30 scheduler.add_job(func=date_test, args=('一次性任务,会出错',), next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=15), id='date_task')
31 scheduler.add_job(func=aps_test, args=('循环任务',), trigger='interval', seconds=3, id='interval_task')
32 scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
33 scheduler._logger = logging #行启用 scheduler 模块的日记记录
34
35 scheduler.start()
结果:
2018-04-05 12:59:29 循环任务
任务照常运行...
2018-04-05 12:59:32 循环任务
任务照常运行...
2018-04-05 12:59:35 循环任务
任务照常运行...
2018-04-05 12:59:38 循环任务
任务照常运行...
2018-04-05 12:59:41 一次性任务,会出错
任务出错了!!!!!!
2018-04-05 12:59:41 循环任务
任务照常运行...
2018-04-05 12:59:44 循环任务
任务照常运行...
2018-04-05 12:59:47 循环任务
任务照常运行...
转自下面:
版权声明:本文为CSDN博主「清如許」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/somezz/article/details/83104368