第三十四章:APSchudler

一:APSchudler

APSchedule:
https://apscheduler.readthedocs.io/en/3.x/userguide.html 【官网】
https://blog.csdn.net/qq_41341757/article/details/118759836

1.APSchudler 介绍

1.安装

pip install apscheduler

如果您没有安装 pip,可以通过下载并运行 get-pippy 轻松安装它。
如果由于某种原因,pip 无法工作,您可以从 PyPI 手动下载 APScheduler 发行版,解压缩并安装它

python setup.py install

2.架构

1:触发器: 调度逻辑,描述任务何时被触发。(日期触发,时间间隔,cronjob表达式)
2:作业存储区:指定作业存储的位置,默认是保存在内存中,
3:执行器:将任务(函数)提交到线程池或者进程持中运行,当任务完成时,通知调度器发生相应的事件。
4:调度程序:任务调度器,属于控制角色,通过它配置作业存储器、执行器和触发器,添加、修改和删除任务。调度器协调触发器、作业存储器、执行器的运行,通常只有一个调度程序运行在应用程序中,开发人员通常不需要直接处理作业存储器、执行器或触发器,配置作业存储器和执行器是通过调度器来完成的。

2.1 调度器

# 调度器的执行原理:
	1.循环询问作业存储器,有没有到期要执行的任务,如果有则计算运行的时间点。
	2.交给执行器按照时间点运行。

# 调度器的分类:
	BlockingScheduler: 调用start后会阻塞主线程。
	BackgroundScheduler: 调用start后默认开启守护线程,不会阻塞主线程。
	AsyncIOScheduler: 与AsyncIO配合使用
	GeventScheduler: 与Gevent配合使用
	TwistedScheduler: 与Twisted配合使用
	QtScheduler: 与Qt配合使用

2.2 作业存储器

# 内存
	程序崩溃,则重启时,重新加入任务。
# 数据库
	程序崩溃,重启时,恢复中断的状态。推荐使用:PostgreSQL

2.3 执行器

1.线程池执行器:默认
2.进程池执行器:CPU密集型
3.线程池+进程池执行器:

二:基本参数

1.触发器

scheduler.add_job(func=my_task1, trigger="interval", minutes=1)
触发器 描述
date 日期:触发任务运行的具体日期
interval 间隔:触发任务运行的时间间隔
cron 触发任务运行的周期

2.时间

scheduler.add_job(func=my_task1, trigger="interval", minutes=1)
scheduler.add_job(func=my_task2, trigger="interval", minutes=1, end_date="2019-6-9 17:43:00")
字段 描述 间隔
month 月份 1-12
weeks 每隔多少周后执行一次 1-53
days 每隔多少天后执行一次 1-31
hours 每隔多少小时后执行一次 0-23
minutes 每隔多少分钟后执行一次 0-59
seconds 每隔多少秒后执行一次 0-59
start_date 任务触发的起始时间
end_date 任务触发的结束时间
timezone 时区
jitter 随机的浮动秒数

此外还可以指定 start_dateend_date,表示任务触发的 起始时间结束时间

比如某个任务每隔一天执行一次,但是这个任务有截止日期,当超过了截止日期的时候,就不需要再执行它了。

于是就可以将该 截止日期 设置为 end_date,如果超过了,那么任务会被取消掉

另外还有一个 jitter 当所有任务全部都在一起执行的时候,可能造成服务器资源压力大,那么添加一个随机秒数,可以避免造成服务拥堵。参数,表示添加一个随机的浮动秒数。

# 除了add_job的方式,我们还可以通过 scheduled_job 使用装饰器的方式
@scheduler.scheduled_job(trigger="interval", seconds=10, jitter=1)

3.表达式类型

表达式 意义 描述
* 所有 通配符,例如: minutes=*,表示每分钟触发
*/a 所有 可被 a 整除的通配符
a-b 所有 范围 a-b 触发
a-b/c 所有 范围 a-b、且能被 C 整除时 触发
xth y 第几个星期几触发。x为第几个,y为星期几
last x 一个月中,最后一个星期几触发
last 一个月最后一天触发
x,y,z 所有 组合表达式,可以组合确定值或上方的表达式
# hour=19,minute=23 这里表示每个月的每一天在 19:23 的时候执行任务。因为显式指定的参数的前面的参数都被设置为 *,表示"每";此外也可以是 hour ='19', minute ='23',可以填字符串也可以填数字

# month='6-9,11-12',day='4rd sun',hour='0-3' 表示将在第 6、7、8、9、11、12 个月的第四个星期日的00:00:00、01:00:00、02:00:00、03:00:00执行任务

# day=15,hour=20,minute=14 表示每个月的第 15 天的 20:14 的时候执行任务
# day='4rd sun' 表示将在每个月的第四个周日执行任务,而且是 00:00:00,因为后面的参数如果不指定的话,默认为最小值
# day='last sun',hour=17,minute=25 表示每个月的最后一个星期日的 17:25 执行任务
# day='last',hour=20 表示每个月的最后一天的 20:00 的时候执行任务
# day_of_week='0-2' 表示每一周的周一、周二、周三执行任务

# month='1-3',day_of_week='mon',hour='22',minute='14',second='48' 表示 1 月、2 月、3 月的每个星期 1 的 22:14:48 的时候执行任务

三:基本案例

1.每隔三秒钟执行一次

from datetime import datetime
import os
# 1: 导入这个最简单的调度器
from apscheduler.schedulers.blocking import BlockingScheduler

# 2: 定义我们的job
def tick():
    print("Tick! The time is: %s" % datetime.now())


if __name__ == '__main__':
    # 3: 实例化BlockingScheduler调度器,没有参数表示存储器是:内存
    # 执行器是线程池,默认的线程并发数是10个。
    scheduler = BlockingScheduler()
    # 4:调度器绑定任务,并指定触发器
    # 触发器:‘interval’表示间隔执行, ‘date’, 表示指定时间触发, ‘cron’表示固定时间间隔触发。
    scheduler.add_job(tick, 'interval', seconds=3)

    try:
        # 5:执行任务
        scheduler.start()
    except Exception as e:
        print(e)

2.每天固定时间执行

from datetime import datetime
import os
# 1: 导入这个最简单的调度器
from apscheduler.schedulers.blocking import BlockingScheduler

# 2: 定义我们的job
def tick():
    print("Tick! The time is: %s" % datetime.now())


if __name__ == '__main__':
    # 3: 实例化BlockingScheduler调度器,没有参数表示存储器是:内存
    # 执行器是线程池,默认的线程并发数是10个。
    scheduler = BlockingScheduler()
    # 4:调度器绑定任务,并指定触发器
    # 每天10点10分执行
    # scheduler.add_job(tick, trigger='cron', hour=10, minute=10)
    # 每天 10点10分,11点10分,12点10分执行
    scheduler.add_job(tick, trigger='cron', hour='10-12', minute=10)

    try:
        # 5:执行任务
        scheduler.start()
    except Exception as e:
        print(e)

3.多执行器,存储器,单调度器案例

案例: 配置两个存储器:一个mongodb,一个sqlite。配置一个线程池执行器和一个进程池执行器。

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

jobstorage = {
    "mongodb": MongoDBJobStore(),
    "default": SQLAlchemyJobStore(url='mongodb数据库地址')
}

executes = {
    "default": ThreadPoolExecutor(20),
    "processpool": ProcessPoolExecutor(5)
}

job_default = {
    # coalesce默认情况下关闭
    'coalesce': False, 
    # 作业的默认最大运行实例限制为3
    "max_instances": 3 
}

scheduler = BackgroundScheduler(jobstores=jobstorage, executors=executes, job_defaults=job_default, timezone=utc)

4.不同存储器执行案例

4.1 内存存储

from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.schedulers.blocking import BlockingScheduler


from datetime import datetime

def my_job(id= 'my_job'):
    print(id, '--->', datetime.now())

# 1: 定义任务存储器为内存,其实默认的也是这个
jobstorage = {
    "default": MemoryJobStore()
}

# 2:定义执行器, 10进程20线程执行
execytors = {
    "default": ThreadPoolExecutor(20),
    "processpoll": ProcessPoolExecutor(10)
}

# 3:定义任务设置
job_defaults = {
    'coalesce': False,
    'max_instances': 3
}

# 4: 实例化调度器
scheduler = BlockingScheduler(jobstorage=jobstorage,
                              execytors=execytors,
                              job_defaults=job_defaults)

# 5: 给调度器增加任务
# 每5分钟执行一次
scheduler.add_job(my_job, args=['job_interval'],
                  id='job_interval',
                  trigger='interval',
                  seconds=5,
                  replace_existing=True)

# 截止到2021-10-25日前,每个4月到8月的每天7点到11点,每10分钟执行一次
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='2021-10-25')
# 默认的配置:立刻执行一次
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='2021-08-05 07:48:05')

4.2 数据库存储

上面代码只需要修改存储器即可

from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.schedulers.blocking import BlockingScheduler


from datetime import datetime

def my_job(id= 'my_job'):
    print(id, '--->', datetime.now())

# 1: 定义任务存储器为内存,其实默认的也是这个
jobstorage = {
    "default": SQLAlchemyJobStore(url="数据库地址")
}

数据库存储存在的问题

  • 假如程序中断了,则再次开启的时候,调度器会将数据库中没有执行的任务再次添加进来。如果我们此时再次运行程序,则优惠追加进来相同的任务。如何让他不再加进来呢?

    # 在追加任务的时候增加配置项: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')
    

四:其他操作

1.调度器的其他操作

scheduler.remove_job(job_id, jobstore=None)  # 删除作业
scheduler.remove_all_jobs(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)  # 输出作业信息

2.调度事件监听

问题1: 如果程序出现异常,会影响整个调度任务吗?

运行这个程序会发现每5分钟报一次错

from apscheduler.schedulers.blocking import BlockingScheduler
import datetime

def aps_test(x):
    print(1/0)
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)

scheduler = BlockingScheduler()
scheduler.add_job(func=aps_test, args=('定时任务',), trigger='cron', second='*/5')
scheduler.start()

问题2:如果程序的一个任务出现异常,其余的任务能正常执行吗?

运行发现,任务一报错,任务二仍然可以运行

from apscheduler.schedulers.blocking import BlockingScheduler
import datetime

def aps_test(x):
    print(1/0)
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)

def aps_test2(x):
    print('哈哈哈哈')

scheduler = BlockingScheduler()
scheduler.add_job(func=aps_test, args=('定时任务',), trigger='cron', second='*/5')
scheduler.add_job(func=aps_test2, args=('定时任务2',), trigger='cron', second='*/5')
scheduler.start()

设置日志记录和事件监听

from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
import datetime
import logging
# 1: 定义日志格式:
# %(levelno)s	打印日志级别的数值
# %(levelname)s	打印日志级别名称
# %(pathname)s	打印当前执行程序的路径,其实就是sys.argv[0]
# %(filename)s	打印当前执行程序名
# %(funcName)s	打印日志的当前函数
# %(lineno)d	打印日志的当前行号
# %(asctime)s	打印日志的记录时间
# %(thread)d	打印线程ID
# %(threadName)s	打印线程的名称
# %(process)d	打印进程的ID
# %(message)s	打印日志的信息
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename='log1.txt',
                    filemode='a')
# 正确的任务
def aps_test(x):
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)
# 出错的任务
def date_test(x):
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), x)
    print(1 / 0)

# 2: 设置监听器
def my_listener(event):
    if event.exception:
        print('任务出错了!!!!!!')
    else:
        print('任务照常运行...')


scheduler = BlockingScheduler()
scheduler.add_job(func=date_test, args=('一次性任务,会出错',),
                  next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=15), id='date_task')
# 每3秒执行一次
scheduler.add_job(func=aps_test, args=('循环任务',), trigger='interval', seconds=3, id='interval_task')

scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
scheduler._logger = logging
scheduler.start()

五:Django框架中使用

1.快速使用

1.1 安装

pip install django-apscheduler 

1.2 配置文件

修改 settings.py 文件

INSTALLED_APPS 中加入 django-apscheduler 应用:

INSTALLED_APPS = [
    ......
    'django_apscheduler',	# 定时执行任务
]

1.3 执行迁移命令

python manage.py migrate

会自动生成两张表:django_apschedule_djangojob(存储任务)和django_apschedule_djangojobexecution(任务执行情况)

1.4 创建任务

两种创建任务的方法 装饰器add_job 函数。添加完成后,数据库中就可以显示

# urls.py  总路由
from django.urls import path, include
urlpatterns = [
    path('', include('apps.APS.urls')),
]
# urls.py  子路由
from APS import views

urlpatterns = [
]

1.4.1 装饰器

装饰器创建任务——适合于写代码的人自己创建任务,在任意 view.py 中实现代码

# view.py

from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job

# 实例化调度器
scheduler = BackgroundScheduler()
# 调度器使用默认的 DjangoJobStore()
scheduler.add_jobstore(DjangoJobStore(), 'default')

# 设置定时任务,选择方式为interval,时间间隔为10s
# 另一种方式为每天固定时间执行任务,对应代码为:
# @register_job(scheduler, 'cron', day_of_week='mon-fri', hour='9', minute='30', second='10',id='task_time')
# replace_existing=True 第二次启动不会报ID已存在的问题,不然每次启动都需要删除库中的ID
@register_job(scheduler, 'interval', id='get_host', hours=6, replace_existing=True, max_instances=100)
def get_host():
    host_all = Host.objects.filter(host_status=0, is_manage=0).values('host_ip', 'host_name')
    variable_all = System_variable.objects.filter(var_group=1).values('var_lable', 'var_name', 'var_value')
    cache.set('host_all', list(host_all))

    variable_dit = {i['var_name']: i for i in variable_all}
    cache.set('variable_all', variable_dit)
    return host_all, variable_dit

1.4.2 add_job

add_job 函数创建任务——适合用户通过页面输入参数,并提交来手动创建定时任务

如果想让用户通过页面输入参数,并提交来手动创建定时任务,就需要使用 add_job 函数。
下面这个小例子,接收 json 数据给后端,触发 insert_cron_job 函数,来添加任务:

import json
from django.http import JsonResponse
from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job


scheduler = BackgroundScheduler()
scheduler.add_jobstore(DjangoJobStore(), 'default')

# cron
def insert_cron_job(json):  # 传入 状态策略 开始任务 结束任务
    # 接收参数
    try:
        # 获取json中对应数据
        # add_job添加任务,传入:my_job为要执行的定时任务,trigger指定任务触发器(triggers)—不同定时模式,args传参,id定时任务命名–id,等其他配置,根据trigger不同,传入不同的 时间格式进行触发
        scheduler.add_job(my_job, trigger="", args=["s", ], id=start_job_id)
    except Exception as e:
        print("获取数据异常")
        return

# 具体要执行的代码
def my_job(s):
    pass
    
register_events(scheduler)
scheduler.start()

注意: 需要加上 replace_existing=True 否则报以下错误,即ID重复

raise ConflictingIdError(job.id)
apscheduler.jobstores.base.ConflictingIdError: 'Job identifier (index_html) conflicts with an existing job'

1.5 删除任务

删除完成后,可以查看数据库中对应的任务id已经删除完成

# remove_job 示例
DjangoJobStore.remove_job(DjangoJobStore(), job_id=需要删除的job_id)
import json
from django.http import JsonResponse
from apscheduler.schedulers.background import BackgroundScheduler
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job


scheduler = BackgroundScheduler()
scheduler.add_jobstore(DjangoJobStore(), 'default')

# cron
def insert_cron_job(json):  # 传入 状态策略 开始任务 结束任务
    # 接收参数
    try:
        # 获取json中对应数据
        # add_job添加任务,传入:my_job为要执行的定时任务,trigger指定任务触发器(triggers)—不同定时模式,args传参,id定时任务命名–id,等其他配置,根据trigger不同,传入不同的 时间格式进行触发
        scheduler.add_job(my_job, trigger="", args=["s", ], id=start_job_id)
    except Exception as e:
        print("获取数据异常")
        return
 
def delete_cron_job(json):
    # 接收参数
    # 获取需要删除的job_id
    try:
        DjangoJobStore.remove_job(DjangoJobStore(), job_id=需要删除的job_id)  
    except Exception as e:
        print("删除定时任务异常:", e)
# 具体要执行的代码
def my_job(s):
    pass
    
register_events(scheduler)
scheduler.start()

1.6 运行项目

python manage.py runserver

2.问题总结

1、实现代码全部写在的 views 中,在顶部导入模块,完成对应初始化工作后,在具体的 views 视图中,根据用户输入的值,使用 add_job 方法完成定时任务的构建;

其他—目前遇到的报错:

# 此错误是因为工作 id 和数据库中现存在工作 id 一致导致的,需要修改 id 名称,或 replace_existing=True【推荐】
Job identifier (test_jobs) conflicts with an existing job

2、django.db.utils.OperationalError: database is locked
初步估计是因为开启的 job 过多,导致连接池不够,改了一种实例化调度器的方式后,可以解决,并且加一个 misfire_grace_time 属性值将这个值设置大一下,增加容错机制

3、当使用 crondata 方法进行定时不能运行问题:

因为在创建调度器实例化时,加入了 timezone='MST' 参数,应该是区时设置的不正确,删除即可

posted @ 2023-04-03 15:46  亦双弓  阅读(147)  评论(0)    收藏  举报