APScheduler是Python中一个强大的第三方库,用于在后台执行定时任务。它允许我们根据设定的时间间隔、日期规则或特定时间来执行任务,适用于定时执行脚本、定时发送邮件、定时处理数据等场景。
用户指南
安装 APScheduler
首选的安装方法是使用 pip:
$ pip install apscheduler
如果没有安装 pip,可以通过下载并运行 get-pip.py 轻松安装。
如果由于某种原因,pip 无法正常工作,您可以从 PyPI 手动下载 APScheduler 发行版,解压缩后安装:
$ python setup.py install
代码示例
源代码发行版包含一个examples目录,在此可以找到许多以不同方式使用 APScheduler 的工作示例。您还可以在线浏览这些示例。
基本概念
APScheduler 有四种组件:
triggers:触发器包含调度逻辑。每个作业都有自己的触发器,它决定作业下一次运行的时间。除了初始配置外,触发器是完全无状态的job stores:作业存储库存放计划作业。默认的作业存储空间只是将作业保存在内存中,但其他存储空间会将作业保存在各种数据库中。作业数据在保存到持久作业存储区时被序列化,从存储区加载回来时又被反序列化。作业存储区(默认存储区除外)不在内存中保存作业数据,而是作为中间人在后台保存、加载、更新和搜索作业。作业存储绝不能在调度程序之间共享。executors:执行器负责处理作业的运行。执行器通常会将作业中指定的可调用程序提交给线程或进程池。作业完成后,执行器会通知调度程序,然后调度程序会发出相应的事件。schedulers:调度程序将其余部分结合在一起。应用程序中通常只运行一个调度程序。应用程序开发人员通常不会直接处理作业存储、执行器或触发器。相反,调度程序会提供适当的接口来处理所有这些工作。配置作业存储和执行器以及添加、修改和删除作业都是通过调度程序来完成的。
选择正确的scheduler、job store、executor和trigger
对调度程序的选择主要取决于你的编程环境和使用 APScheduler 的目的。以下是选择调度程序的快速指南:
BlockingScheduler: 当进程中只有调度程序运行时使用BackgroundScheduler: 当您不使用以下任何框架,并希望调度程序在应用程序后台运行时使用AsyncIOScheduler: 如果您的应用程序使用asyncio模块,请使用GeventScheduler: 如果您的应用程序使用geventTornadoScheduler:如果您正在构建Tornado应用程序,请使用TwistedScheduler: 如果您正在构建Twisted应用程序,请使用QtScheduler: 如果您正在构建Qt应用程序,请使用
很简单吧?
要选择合适的作业存储,需要确定是否需要作业持久性。如果您总是在应用程序启动时重新创建作业,那么您可以使用默认值(MemoryJobStore)。但如果您需要作业在调度程序重启或应用程序崩溃时仍能持久存在,那么您的选择通常取决于您的编程环境中使用了哪些工具。不过,如果您可以自由选择,那么建议选择 PostgreSQL 后端的 SQLAlchemyJobStore,因为它具有强大的数据完整性保护功能。
同样,如果您使用上述框架之一,执行器的选择通常也会由您来做。否则,默认的 ThreadPoolExecutor 对大多数用途来说已经足够了。如果您的工作负载涉及 CPU 密集型操作,则应考虑使用 ProcessPoolExecutor 来充分利用多个 CPU 内核。您甚至可以同时使用这两种执行器,将进程池执行器添加为辅助执行器。
计划作业时,需要为其选择一个触发器。触发器决定了计算作业运行日期/时间的逻辑。APScheduler 有三种内置触发器类型:
也可以将多个触发器合并为一个触发器,在所有参与触发器商定的时间触发,或在任何触发器触发时触发。更多信息,请参阅有关combining triggers(组合触发器)的文档。
您可以在各自的 API 文档页面上找到每个作业存储、执行器和触发器类型的插件名称。
配置scheduler
APScheduler 提供了多种配置调度程序的方法。你可以使用配置字典,也可以将选项作为关键字参数传入。也可以先实例化调度程序,然后添加作业并配置调度程序。这样,你就能在任何环境下获得最大的灵活性。
调度程序级配置选项的完整列表可在 BaseScheduler类的 API 参考资料中找到。调度程序子类还可能有其他选项,这些选项在其各自的 API 参考资料中都有记录。单个作业存储空间和执行器的配置选项同样可以在它们的 API 参考页面上找到。
假设您想在应用程序中使用默认作业存储区和默认执行器运行 BackgroundScheduler:
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
# 在这里或在调度程序初始化之前初始化应用程序的其余部分
这将得到一个 BackgroundScheduler,其中有一个名为 "default "的 MemoryJobStore 和一个名为 "default "的 ThreadPoolExecutor,默认最大线程数为 10。
现在,假设你还想要更多。你想拥有两个作业存储空间,使用两个执行器,还想调整新作业的默认值并设置不同的时区。以下三个示例完全相同,都能满足你的要求:
- 一个名为 "
mongo"的MongoDBJobStore - 一个名为 "default "的
SQLAlchemyJobStore(使用SQLite) - 名为 "default "的
ThreadPoolExecutor,工人数为 20 - 名为 "
processpool"的ProcessPoolExecutor,工人数为 5 - 将
UTC作为调度程序的时区 - 新工作的聚合默认为关闭
- 新工作的默认实例上限为 3 个
示例1:
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, # 关闭工作聚合,默认为False
'max_instances': 3 # 新工作的默认实例上限为 3 个
}
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
示例2:
from apscheduler.schedulers.background import BackgroundScheduler
# "apscheduler. "前缀是硬编码
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',
})
示例3:
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()
# 在这里做些其他事情,也许可以增加工作岗位等。
scheduler.configure(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
启动 scheduler
只需在调度程序上调用 start() 即可启动调度程序。对于 BlockingScheduler 以外的调度程序,该调用会立即返回,你可以继续应用程序的初始化过程,也可以向调度程序添加作业。
对于 BlockingScheduler,只有在完成任何初始化步骤后才需要调用 start()。
调度程序启动后,就不能再更改其设置了。
添加 jobs
有两种方法可以将作业添加到调度程序中:
- 调用
add_job() - 用
scheduled_job()来装饰一个函数
第一种方法是最常见的方法。第二种方法主要是为了方便声明在应用程序运行期间不会改变的作业。add_job() 方法会返回一个 apscheduler.job.Job 实例,以后可以用它来修改或删除作业。
您可以随时在scheduler上调度jobs。如果添加job时scheduler尚未运行,job将被暂定调度,只有当scheduler启动时才会计算其首次运行时间。
方式1:
scheduler.add_job(
task_demo, 'interval', seconds=15, id='task_demo', replace_existing=True, start_date=datetime.datetime.now()
)
方式2:
@scheduler.scheduled_job('interval', seconds=15, id='task_demo',
start_date=datetime.datetime.now())
def task_demo():
log.info("task_demo.............................")
需要注意的是,如果您使用的executors或jobstores将作业序列化,那么它将为您的作业增加一些要求:
-
目标可调用程序必须可全局访问
-
任何可调用参数都必须是可序列化的
在内置jobstores中,只有 MemoryJobStore 不会序列化jobs。在内置executors中,只有 ProcessPoolExecutor 会序列化jobs。
如果在应用程序初始化期间在持久任务存储区中安排任务,则必须为任务定义显式 ID 并使用 replace_existing=True,否则每次重启应用程序时都会获得新的任务副本!
要立即运行job,请在添加job时省略trigger参数。
删除jobs
从调度程序中移除作业时,该作业会从相关的作业存储中移除,不再执行。有两种方法可以做到这一点:
- 通过调用
remove_job(),使用job的ID和job存储别名 - 在通过
add_job()获得的工作实例上调用remove()
后一种方法可能更方便,但需要在某处存储添加作业时收到的作业实例。对于通过 scheduled_job() 调度的作业,第一种方法是唯一的方法。
如果作业的计划结束(即其触发器不会产生任何进一步的运行时间),则会自动将其删除。
例子:
job = scheduler.add_job(myfunc, 'interval', minutes=2)
job.remove()
同样,使用显式 Job ID:
scheduler.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
scheduler.remove_job('my_job_id')
暂停和恢复 Jobs
您可以通过 Job 实例或计划程序本身轻松暂停和恢复作业。暂停作业时,将清除其下一次运行时间,并且在恢复作业之前不会为其计算进一步的运行时间。要暂停作业,请使用以下任一方法:
要恢复:
获取计划作业列表
若要获取计划作业的机器可处理列表,可以使用该方法 get_jobs() 。它将返回实例列表 Job 。如果您只对特定作业存储中包含的作业感兴趣,请将作业存储别名作为第二个参数。
为方便起见,您可以使用该方法 print_jobs() 打印出作业、触发器和下一次运行时间的格式化列表。
修改 Jobs
可以通过调用 apscheduler.job.Job.modify() 或 来 modify_job() 修改任何作业属性。您可以修改除 以外的 id 任何作业属性。
例:
job.modify(max_instances=6, name='Alternate name')
如果要重新计划作业(即更改其触发器),可以使用 apscheduler.job.Job.reschedule() 或 reschedule_job() 。这些方法为作业构造一个新触发器,并根据新触发器重新计算其下一个运行时间。
例:
scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')
关闭调度程序
要关闭调度程序:
scheduler.shutdown()
默认情况下,调度程序会关闭其作业存储空间和执行器,并等待所有当前执行的作业完成。如果不想等待,可以这样做:
scheduler.shutdown(wait=False)
这仍将关闭作业存储空间和执行器,但不会等待任何运行中的任务完成。
暂停/恢复Jobs处理
可以暂停处理计划作业:
scheduler.pause()
这将导致调度程序在恢复处理之前不会被唤醒:
scheduler.resume()
也可以在暂停状态下启动调度程序,即不进行第一次唤醒:
scheduler.start(paused=True)
当你需要在不需要的任务有机会运行之前对其进行修剪时,这很有用。
限制作业并发执行实例的数量
默认情况下,每个作业只能同时运行一个实例。这意味着,如果作业即将运行,但前次运行尚未结束,那么最近一次运行将被视为误运行。通过在添加作业时使用 max_instances 关键字参数,可以设置调度程序允许同时运行的特定作业的最大实例数。
错过作业执行和合并
有时,调度程序可能无法在预定运行时间执行预定作业。最常见的情况是,在持久任务存储区中调度了一个任务,而调度程序在该任务本应执行后被关闭并重新启动。发生这种情况时,作业会被视为 "误执行"。然后,调度程序将根据作业的 misfire_grace_time 选项(可按作业或在调度程序中全局设置)检查每次错过的执行时间,以确定是否仍应触发执行。这可能导致作业被连续执行多次。
如果您的特定用例不希望出现这种行为,可以使用聚合将所有这些错过的执行合并为一次。换句话说,如果为作业启用了聚合,并且调度程序看到一个或多个排队执行的作业,则只会触发一次。被 "绕过 "的运行将不会发送失火事件。
如果由于线程或进程池中没有可用的线程或进程而导致作业执行延迟,执行器可能会跳过该作业,因为它运行得太晚了(与最初指定的运行时间相比)。如果您的应用程序中可能会出现这种情况,您可能需要增加执行器中的线程/进程数量,或者将 misfire_grace_time 设置调高。
调度程序事件
调度程序可以附加事件监听器。调度程序事件在特定场合触发,其中可能包含有关该特定事件细节的附加信息。通过向 add_listener()提供适当的掩码参数,将不同的常量OR在一起,可以只监听特定类型的事件。监听器调用时只有一个参数,即事件对象。
有关可用事件及其属性的详细信息,请参阅 events模块的文档。
例:
def my_listener(event):
if event.exception:
print('The job crashed :(')
else:
print('The job worked :)')
scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
故障排除
如果调度程序没有按预期运行,将 apscheduler 日志记录器的日志记录级别提高到 DEBUG 级别会很有帮助。
如果尚未启用日志记录,则可以执行以下操作:
import logging
logging.basicConfig()
logging.getLogger('apscheduler').setLevel(logging.DEBUG)
这应该提供有关调度程序内部发生的事情的大量有用信息。
此外,请务必查看 常见问题 部分,看看您的问题是否已经有了解决方案。