Python定时任务框架之Apscheduler 案例分享

  引言

  前面已经讲过Celery做定时任务的场景,现在分享另一个框架Apscheduler。Apscheduler的全称是Advanced Python Scheduler。它是一个轻量级的 Python 定时任务调度框架。同时,它还支持异步执行、后台执行调度任务。本人小小的建议是一般项目用APScheduler,因为不用像Celery那样再单独启动worker、beat进程,而且API也很简洁。

  需求背景

  前端时间双十一公司业务暴增的情况下,订单也是暴增,要在钉钉群定时播报关键的业务数据,这个时候需要一个简洁又快速出结果的方案。于是偷偷用python花了不到半个小时写了一个不到30行的脚本(包括调试),完成了领导的需求。

  简介

  Apscheduler的官方文档可以参考:https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html#module-apscheduler.triggers.cron  或:https://apscheduler.readthedocs.io/en/latest/userguide.html#

  Python定时任务框架APScheduler,Advanced Python Scheduler (APScheduler) 是一个轻量级但功能强大的进程内任务调度器,作用为在指定的时间规则执行指定的作业(时间规则:指定的日期时间、固定时间间隔以及类似Linux系统中Crontab的方式);并且该框架可以进行持久化配置,保证在项目重启或者崩溃恢复后仍然能够恢复之前的作业继续运行。

  特点

  1、不依赖于Linux系统的crontab系统定时,独立运行

  2、可以动态添加新的定时任务,如下单后30分钟内必须支付,否则取消订单,就可以借助此工具(每下一单就要添加此订单的定时任务)

  3、对添加的定时任务可以做持久保存

  四大组件

  触发器(triggers):触发器包含调度逻辑,描述一个任务何时被触发,按日期或按时间间隔或按 cronjob 表达式三种方式触发。每个作业都有它自己的触发器,除了初始配置之外,触发器是完全无状态的。

  作业存储器(job stores):作业存储器指定了作业被存放的位置,默认情况下作业保存在内存,也可将作业保存在各种数据库中,当作业被存放在数据库中时,它会被序列化,当被重新加载时会反序列化。作业存储器充当保存、加载、更新和查找作业的中间商。在调度器之间不能共享作业存储。

  执行器(executors):执行器是将指定的作业(调用函数)提交到线程池或进程池中运行,当任务完成时,执行器通知调度器触发相应的事件。

  调度器(schedulers):任务调度器,属于控制角色,通过它配置作业存储器、执行器和触发器,添加、修改和删除任务。调度器协调触发器、作业存储器、执行器的运行,通常只有一个调度程序运行在应用程序中,开发人员通常不需要直接处理作业存储器、执行器或触发器,配置作业存储器和执行器是通过调度器来完成的。

  重要组件说明

  触发器(triggers)——目前APScheduler支持触发器:

DateTrigger
IntervalTrigger
CronTrigger

  DateTrigger: 指定日期时间执行一次

  IntervalTrigger: 固定时间间隔执行,支持每秒、每分、每时、每天、每周

  CronTrigger: 类似Linux系统的Crontab定时任务

  DateTrigger和IntervalTrigger很好理解,使用也比较简单,这里重点说一下CronTrigger触发器。

  CronTrigger触发器的参数选项如下:

  CronTrigger可用的表达式:

  执行器(executors)——目前APScheduler支持的Executor:

AsyncIOExecutor
GeventExecutor
ThreadPoolExecutor
ProcessPoolExecutor
TornadoExecutor
TwistedExecutor

  作业存储器(job stores)——目前APScheduler支持的Jobstore:

MemoryJobStore
MongoDBJobStore
RedisJobStore
RethinkDBJobStore
SQLAlchemyJobStore
ZooKeeperJobStore

  调度器(schedulers)——目前APScheduler支持的Scheduler:

AsyncIOScheduler
BackgroundScheduler --非阻塞方式
BlockingScheduler   --阻塞方式
GeventScheduler
QtScheduler
TornadoScheduler
TwistedScheduler

  Job作业——Job作为APScheduler最小执行单位。创建Job时指定执行的函数,函数中所需参数,Job执行时的一些设置信息。

id:指定作业的唯一ID

name:指定作业的名字

trigger:apscheduler定义的触发器,用于确定Job的执行时间,根据设置的trigger规则,计算得到下次执行此job的
时间, 满足时将会执行

executor:apscheduler定义的执行器,job创建时设置执行器的名字,根据字符串你名字到scheduler获取到执行此
job的 执行器,执行job指定的函数

max_instances:执行此job的最大实例数,executor执行job时,根据job的id来计算执行次数,根据设置的最大实例数
来确定是否可执行

next_run_time:Job下次的执行时间,创建Job时可以指定一个时间[datetime],不指定的话则默认根据trigger获取触
发时间

misfire_grace_time:Job的延迟执行时间,例如Job的计划执行时间是21:00:00,但因服务重启或其他原因导致
21:00:31才执行,如果设置此key为40,则该job会继续执行,否则将会丢弃此job

coalesce:Job是否合并执行,是一个bool值。例如scheduler停止20s后重启启动,而job的触发器设置为5s执行
一次,因此此job错过了4个执行时间,如果设置为是,则会合并到一次执行,否则会逐个执行

func:Job执行的函数

args:Job执行函数需要的位置参数

kwargs:Job执行函数需要的关键字参数

  创建步骤

  基本分为四个步骤:创建调度器→添加调度任务/触发器(满足条件)→执行器

# 1.创建调度器
# 后台执行  此处程序不会发生阻塞
scheduler = BackgroundScheduler()

# 2.添加调度任务
# 3.触发器triggers='interval'
# 每隔20秒执行一次
scheduler.add_job(main, 'interval', seconds=20)

# 4.满足条件执行器
scheduler.start()

  触发器 Trigger使用三种场景

  date——定时调度(在特定的时间日期执行,作业只会执行一次)

from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler

sched = BlockingScheduler()
def my_job():
	print(1)
# The job will be executed on November 6th, 2009
sched.add_job(my_job, 'date', run_date=date(2009, 11, 6), args=['text'])
# The job will be executed on November 6th, 2009 at 16:30:05
sched.add_job(my_job, 'date', run_date=datetime(2009, 11, 6, 16, 30, 5), args=['text'])

  interval——间隔调度(每隔多久执行一次)

from datetime import datetime
import os
from apscheduler.schedulers.blocking import BlockingScheduler

def tick():
    print('Tick! The time is: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    # sep1 每隔3秒执行一次
    scheduler.add_job(tick, 'interval', seconds=3)
    # sep2 表示每隔3天17时19分07秒执行一次任务
    scheduler.add_job(tick, 'interval', days=3, hours=17, minutes=19, seconds=7)
    # 每20秒执行一次
    scheduler.add_job(tick, 'interval', seconds=61)

    print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C    '))

    try:
        scheduler.start()
    except (KeyboardInterrupt, SystemExit):
        pass

   cron——某一定时时间执行(按指定的周期执行):

from datetime import datetime
import os
from apscheduler.schedulers.blocking import BlockingScheduler

def tick():
    print('Tick! The time is: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    # 表示每天的19:23 分执行任务
    scheduler.add_job(tick, 'cron', hour=19,minute=23)
    # 每天8点整执行
    scheduler.add_job(tick, 'cron', day_of_week='0-6', hour=8, minute=00, second=00)
    # 每天0点,1点,8点执行
    scheduler.add_job(tick,'cron',month='*', day='*', hour='0,1,8',minute='00')
    
    # 表示2017年3月22日17时19分07秒执行该程序
    scheduler.add_job(tick, 'cron', year=2017, month=3, day=22, hour=17, minute=19, second=7)

    # 表示任务在6,7,8,11,12月份的第三个星期五的00:00,01:00,02:00,03:00 执行该程序
    scheduler.add_job(tick, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3')

    # 表示从星期一到星期五5:30(AM)直到2014-05-30 00:00:00
    scheduler.add_job(tick, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2014-05-30')

    # 表示每5秒执行该程序一次,相当于interval 间隔调度中seconds = 5
    scheduler.add_job(tick, 'cron', second='*/5')


    print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C    '))

    try:
        scheduler.start()
    except (KeyboardInterrupt, SystemExit):
        pass

  

  完整Demo

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time    : 2020/11/17 0017 22:16
# @Author  : liudinglong
# @File    : send_demo.py
# @Description: 
# @Question: 
'''

from datetime import datetime
import json
import urllib.request
import pymysql as pms
from apscheduler.schedulers.background import BackgroundScheduler,BlockingScheduler
import os
# Mac下关闭ssl验证用到以下模块
import ssl

'''
----------------------------------------------
# 需要CMD命令下安装以下支持库:
# pip install apscheduler
# pip install pymysql
----------------------------------------------
'''
# Mac和Linux下关闭ssl验证,不然会报错
ssl._create_default_https_context = ssl._create_unverified_context

# 你的钉钉机器人url
global myurl
my_url = "https://oapi.dingtalk.com/robot/send?access_token=XXXXXXXXXXXX"


def send_request(url, datas):
    # 传入url和内容发送请求
    # 构建一下请求头部
    header = {
        "Content-Type": "application/json",
        "Charset": "UTF-8"
    }
    sendData = json.dumps(datas)  # 将字典类型数据转化为json格式
    sendDatas = sendData.encode("utf-8")  # python3的Request要求data为byte类型
    # 发送请求
    request = urllib.request.Request(url=url, data=sendDatas, headers=header)
    # 将请求发回的数据构建成为文件格式
    opener = urllib.request.urlopen(request)
    # 打印返回的结果
    print(opener.read())


def get_mysqldatas(sql):
    # 一个传入sql导出数据的函数,实例为MySQL需要先安装pymysql库,cmd窗口命令:pip install pymysql
    # 跟数据库建立连接
    conn = pms.connect(host='服务器地址', user='用户名', passwd='密码', database='数据库', port=3306, charset="utf8")
    # 使用 cursor() 方法创建一个游标对象
    cur = conn.cursor()
    # 使用 execute() 方法执行 SQL
    cur.execute(sql)

    # 获取所需要的数据
    datas = cur.fetchall()

    # 关闭连接
    cur.close()
    # 返回所需的数据
    return datas


def get_ddmodel_datas(type):
    # 返回钉钉模型数据,1:文本;2:markdown所有人;3:markdown带图片,@接收人;4:link类型
    if type == 1:
        my_data = {
            "msgtype": "text",
            "text": {
                "content": " "
            },
            "at": {
                "atMobiles": [
                    "188XXXXXXX"
                ],
                "isAtAll": False
            }
        }
    elif type == 2:
        my_data = {
            "msgtype": "markdown",
            "markdown": {"title": " ",
                         "text": " "
                         },
            "at": {
                "isAtAll": True
            }
        }
    elif type == 3:
        my_data = {
            "msgtype": "markdown",
            "markdown": {"title": " ",
                         "text": " "
                         },
            "at": {
                "atMobiles": [
                    "188XXXXXXXX"
                ],
                "isAtAll": False
            }
        }
    elif type == 4:
        my_data = {
            "msgtype": "link",
            "link": {
                "text": " ",
                "title": " ",
                "picUrl": "",
                "messageUrl": " "
            }
        }
    return my_data


def main():
    print('Main! The time is: %s' % datetime.now())
    # 按照钉钉给的数据格式设计请求内容 链接https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.p7hJKp&treeId=257&articleId=105735&docType=1
    # 调用钉钉机器人全局变量myurl
    global myurl

    # 1.Text类型群发消息
    # 合并标题和数据
    My_content = "hello,  @188XXXXXXXX 这是一个测试消息"
    my_data = get_ddmodel_datas(1)
    # 把文本内容写入请求格式中
    my_data["text"]["content"] = My_content
    send_request(my_url, my_data)

    # 2.Markdown类型群发消息(MySQL查询结果发送)
    # 获取sql数据
    sql = "SELECT branch_no,count(*) from wzy_customer_user group by branch_no order by branch_no"
    my_mydata = get_mysqldatas(sql)
    str1 = '\t\n\r'
    seq = []
    for i in range(len(my_mydata)):
        seq.append(str(my_mydata[i]))
    data = str1.join(seq)
    data = data.replace('\'', '')
    data = data.replace('(', '')
    data = data.replace(')', '')
    data = data.replace(',', '\t')
    print(data)

    Mytitle = "#### XXX报表\r\n单位\t数量\t\n\r %s"
    my_Mytitle = Mytitle.join('\t\n') % data
    my_data = get_ddmodel_datas(2)
    my_data["markdown"]["title"] = "XXXX 通报"
    my_data["markdown"]["text"] = my_Mytitle
    send_request(my_url, my_data)

    # 3.Markdown(带图片@对象)
    my_data = get_ddmodel_datas(3)
    my_data["markdown"]["title"] = "系统预警"
    my_data["markdown"][
        "text"] = "#### 系统预警内容  \n > @188XXXXXXXX \n\n > ![screenshot](http://i01.lw.aliimg.com/media/lALPBbCc1ZhJGIvNAkzNBLA_1200_588.png)\n  > ###### 20点00分发布 [详情](http://www.baidu.cn/)"
    send_request(my_url, my_data)
    # 字体颜色:<font color='#FF4500' size=2>%s</font> 双\n\n表示换行
    # 4.Link类型群发消息
    my_data = get_ddmodel_datas(4)
    my_data["link"]["text"] = "群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。 "
    my_data["link"]["title"] = "自定义机器人协议"
    my_data["link"][
        "messageUrl"] = "https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Rqyvqo&treeId=257&articleId=105735&docType=1"
    send_request(my_url, my_data)


if __name__ == "__main__":
    # 定时执行任务,需要先安装apscheduler库,cmd窗口命令:pip install apscheduler
    # 随脚本执行
    # 1.创建调度器
    # scheduler = BlockingScheduler() --阻塞方式
    # 后台执行  此处程序不会发生阻塞
    scheduler = BackgroundScheduler()

    # 2.添加调度任务
    # 3.触发器triggers='interval'
    # 每隔20秒执行一次
    scheduler.add_job(main, 'interval', seconds=20)
    '''
    ***定时执行示例***
    #固定时间执行一次
    #sched.add_job(main, 'cron', year=2018, month=9, day=28, hour=15, minute=40, second=30)
    #表示2017年3月22日17时19分07秒执行该程序
    scheduler.add_job(my_job, 'cron', year=2017,month = 03,day = 22,hour = 17,minute = 19,second = 07)

    #表示任务在6,7,8,11,12月份的第三个星期五的00:00,01:00,02:00,03:00 执行该程序
    scheduler.add_job(my_job, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3')

    #表示从星期一到星期五5:30(AM)直到2014-05-30 00:00:00
    scheduler.add_job(my_job(), 'cron', day_of_week='mon-fri', hour=5, minute=30,end_date='2014-05-30')

    #表示每5秒执行该程序一次,相当于interval 间隔调度中seconds = 5
    scheduler.add_job(my_job, 'cron',second = '*/5')
    '''
    # 4.满足条件执行器
    scheduler.start()
    print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
    try:
        # 其他任务是独立的线程执行
        while True:
            pass
            # time.sleep(60)
            # print('进程正在执行!')
    except (KeyboardInterrupt, SystemExit):
        # 终止任务
        scheduler.shutdown()
        print('Exit The Job!')

  使用案例——钉钉群定时播报消息

  1、在钉钉群助手中,自定义一个机器人,如图:

 

   代码设置10秒发送一次,具体如下:

scheduler.add_job(main,'interval',seconds=10)

  运行结果:

 

截图如下:

 

 

 

  脚本部署

  定时任务的脚本在一定时期内是需要持久使用,如果用IDE跑肯定不方面,于是将它弄到服务器上。

  先把脚本上传到服务器上,然后按照相关的库,最后就是启动,在Linux启动方式如下:

linux命令运行py脚本:nohup python -u test.py > out.log 2>&1 &

  

 

   日志:

  

 

   这里需要注意的是,参数使用-u的意义:

python的输出有缓冲,导致out.log并不能够马上看到输出。
-u 参数,使得python不启用缓冲。

  nohup就是不挂起的意思( no hang up)。该命令的一般形式为:nohup ./test & 

  末尾加个&是指在后台运行,不会因为终端关闭或断开连接而终止程序。

  具体可以参考:https://www.runoob.com/linux/linux-comm-nohup.html

  这样就启动了一个py服务。

 

  总结

  对定时任务框架Apscheduler的简单使用到此。在工作中遇到其他需要,可以进一步了解,学习是为了解决问题,为了更好的工作。同时,欢迎小伙伴进去沟通交流测试心得与工作方法。

 

posted @ 2020-11-17 23:51  全栈测试开发日记  阅读(1028)  评论(0编辑  收藏  举报