python定時(shí)任務(wù)框架apscheduler

前言

說到定時(shí)任務(wù),你會(huì)想起 linux 自帶的 crontab ,windows 自帶的任務(wù)計(jì)劃,都可以實(shí)現(xiàn)守時(shí)任務(wù)。沒錯(cuò),操作系統(tǒng)基本都會(huì)提供定時(shí)任務(wù)的實(shí)現(xiàn),但是如果你想要更加精細(xì)化的控制,或者說任務(wù)程序需要跨平臺(tái)運(yùn)行,最好還是自己實(shí)現(xiàn)定時(shí)任務(wù)框架,Python 的 apscheduler 提供了非常豐富而且方便易用的定時(shí)任務(wù)接口。本文介紹如何使用 apscheduler 實(shí)現(xiàn)你的定時(shí)任務(wù)。

apscheduler 使用起來十分方便。提供了基于日期、固定時(shí)間間隔以及crontab 類型的任務(wù),我們可以在主程序的運(yùn)行過程中快速增加新作業(yè)或刪除舊作業(yè),如果把作業(yè)存儲(chǔ)在數(shù)據(jù)庫中,那么作業(yè)的狀態(tài)會(huì)被保存,當(dāng)調(diào)度器重啟時(shí),不必重新添加作業(yè),作業(yè)會(huì)恢復(fù)原狀態(tài)繼續(xù)執(zhí)行。apscheduler 可以當(dāng)作一個(gè)跨平臺(tái)的調(diào)度工具來使用,可以做為 linux 系統(tǒng)crontab 工具或 windows 計(jì)劃任務(wù)程序的替換。注意,apscheduler 不是一個(gè)守護(hù)進(jìn)程或服務(wù),它自身不帶有任何命令行工具。它主要是要在現(xiàn)有的應(yīng)用程序中運(yùn)行,也就是說,apscheduler 為我們提供了構(gòu)建專用調(diào)度器或調(diào)度服務(wù)的基礎(chǔ)模塊。

安裝

安裝非常簡(jiǎn)單,會(huì)用 pip 的人都知道

pip install apscheduler

基本概念介紹

觸發(fā)器(triggers):觸發(fā)器包含調(diào)度邏輯,描述一個(gè)任務(wù)何時(shí)被觸發(fā),按日期或按時(shí)間間隔或按 cronjob 表達(dá)式三種方式觸發(fā)。每個(gè)作業(yè)都有它自己的觸發(fā)器,除了初始配置之外,觸發(fā)器是完全無狀態(tài)的。

作業(yè)存儲(chǔ)器(job stores):作業(yè)存儲(chǔ)器指定了作業(yè)被存放的位置,默認(rèn)情況下作業(yè)保存在內(nèi)存,也可將作業(yè)保存在各種數(shù)據(jù)庫中,當(dāng)作業(yè)被存放在數(shù)據(jù)庫中時(shí),它會(huì)被序列化,當(dāng)被重新加載時(shí)會(huì)反序列化。作業(yè)存儲(chǔ)器充當(dāng)保存、加載、更新和查找作業(yè)的中間商。在調(diào)度器之間不能共享作業(yè)存儲(chǔ)。

執(zhí)行器(executors):執(zhí)行器是將指定的作業(yè)(調(diào)用函數(shù))提交到線程池或進(jìn)程池中運(yùn)行,當(dāng)任務(wù)完成時(shí),執(zhí)行器通知調(diào)度器觸發(fā)相應(yīng)的事件。

調(diào)度器(schedulers):任務(wù)調(diào)度器,屬于控制角色,通過它配置作業(yè)存儲(chǔ)器、執(zhí)行器和觸發(fā)器,添加、修改和刪除任務(wù)。調(diào)度器協(xié)調(diào)觸發(fā)器、作業(yè)存儲(chǔ)器、執(zhí)行器的運(yùn)行,通常只有一個(gè)調(diào)度程序運(yùn)行在應(yīng)用程序中,開發(fā)人員通常不需要直接處理作業(yè)存儲(chǔ)器、執(zhí)行器或觸發(fā)器,配置作業(yè)存儲(chǔ)器和執(zhí)行器是通過調(diào)度器來完成的。

調(diào)度器的工作流程

實(shí)例1 -間隔性任務(wù)
# -*- coding: utf-8 -*-
# Time: 2018/10/13 19:01:30
# File Name: ex_interval.py

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

def tick():
    print('觸發(fā)!現(xiàn)在時(shí)間是: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    scheduler.add_job(tick, 'interval', seconds=3)
    print('請(qǐng)按下Ctrl+{0}鍵退出'.format('Break' if os.name == 'nt' else 'C    '))

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

說明
第 1 行代碼聲明文件內(nèi)容以 utf-8 編碼,告訴Python 解釋器以 utf-8 編碼解析源代碼文件。
導(dǎo)入 datetime 模塊,用于打印當(dāng)前時(shí)間。導(dǎo)入 os 模塊,用于判斷操作系統(tǒng)類型。
導(dǎo)入調(diào)度器模塊 BlockingScheduler,這是最簡(jiǎn)單的調(diào)度器,調(diào)用 start 方阻塞當(dāng)前進(jìn)程,如果你的程序只用于調(diào)度,除了調(diào)度進(jìn)程外沒有其他后臺(tái)進(jìn)程,那么 BlockingScheduler 非常有用,此時(shí)調(diào)度進(jìn)程相當(dāng)于守護(hù)進(jìn)程。
定義一個(gè)函數(shù) tick 代表我們要調(diào)度的作業(yè)程序。
實(shí)例化一個(gè) BlockingScheduler 類,不帶參數(shù)表明使用默認(rèn)的作業(yè)存儲(chǔ)器-內(nèi)存,默認(rèn)的執(zhí)行器是線程池執(zhí)行器,最大并發(fā)線程數(shù)默認(rèn)為 10 個(gè)(另一個(gè)是進(jìn)程池執(zhí)行器)。
第 11 行添加一個(gè)作業(yè) tick,觸發(fā)器為 interval,每隔 3 秒執(zhí)行一次,另外的觸發(fā)器為 date,cron。date 按特定時(shí)間點(diǎn)觸發(fā),cron 則按固定的時(shí)間間隔觸發(fā)。
加入捕捉用戶中斷執(zhí)行和解釋器退出異常,pass 關(guān)鍵字,表示什么也不做。

實(shí)例2 - cron 任務(wù)
# -*- coding: utf-8 -*-
# Time: 2018/10/13 19:21:09
# File Name: ex_cron.py


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

def tick():
    print('觸發(fā)!現(xiàn)在時(shí)間是: %s' % datetime.now())

if __name__ == '__main__':
    scheduler = BlockingScheduler()
    scheduler.add_job(tick, 'cron', hour=19, minute=23)
    print('請(qǐng)按下Ctrl+{0}鍵退出'.format('Break' if os.name == 'nt' else 'C    '))

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

配置調(diào)度器

調(diào)度器的主循環(huán)其實(shí)就是反復(fù)檢查是不是有到時(shí)需要執(zhí)行的任務(wù),分以下幾步進(jìn)行:

詢問自己的每一個(gè)作業(yè)存儲(chǔ)器,有沒有到期需要執(zhí)行的任務(wù),如果有,計(jì)算這些作業(yè)中每個(gè)作業(yè)需要運(yùn)行的時(shí)間點(diǎn),如果時(shí)間點(diǎn)有多個(gè),做 coalesce 檢查。
提交給執(zhí)行器按時(shí)間點(diǎn)運(yùn)行。
在配置調(diào)度器前,我們首先要選取適合我們應(yīng)用環(huán)境場(chǎng)景的調(diào)度器,存儲(chǔ)器和執(zhí)行器。下面是各調(diào)度器的適用場(chǎng)景:

BlockingScheduler:適用于調(diào)度程序是進(jìn)程中唯一運(yùn)行的進(jìn)程,調(diào)用start函數(shù)會(huì)阻塞當(dāng)前線程,不能立即返回。
BackgroundScheduler:適用于調(diào)度程序在應(yīng)用程序的后臺(tái)運(yùn)行,調(diào)用start后主線程不會(huì)阻塞。
AsyncIOScheduler:適用于使用了asyncio模塊的應(yīng)用程序。
GeventScheduler:適用于使用gevent模塊的應(yīng)用程序。
TwistedScheduler:適用于構(gòu)建Twisted的應(yīng)用程序。
QtScheduler:適用于構(gòu)建Qt的應(yīng)用程序。

上述調(diào)度器可以滿足我們絕大多數(shù)的應(yīng)用環(huán)境,本文以兩種調(diào)度器為例說明如何進(jìn)行調(diào)度器配置。
作業(yè)存儲(chǔ)器的選擇有兩種:一是內(nèi)存,也是默認(rèn)的配置;二是數(shù)據(jù)庫。具體選哪一種看我們的應(yīng)用程序在崩潰時(shí)是否重啟整個(gè)應(yīng)用程序,如果重啟整個(gè)應(yīng)用程序,那么作業(yè)會(huì)被重新添加到調(diào)度器中,此時(shí)簡(jiǎn)單的選取內(nèi)存作為作業(yè)存儲(chǔ)器即簡(jiǎn)單又高效。但是,當(dāng)調(diào)度器重啟或應(yīng)用程序崩潰時(shí)您需要您的作業(yè)從中斷時(shí)恢復(fù)正常運(yùn)行,那么通常我們選擇將作業(yè)存儲(chǔ)在數(shù)據(jù)庫中,使用哪種數(shù)據(jù)庫通常取決于為在您的編程環(huán)境中使用了什么數(shù)據(jù)庫。我們可以自由選擇,PostgreSQL 是推薦的選擇,因?yàn)樗哂袕?qiáng)大的數(shù)據(jù)完整性保護(hù)。

同樣的,執(zhí)行器的選擇也取決于應(yīng)用場(chǎng)景。通常默認(rèn)的 ThreadPoolExecutor 已經(jīng)足夠好。如果作業(yè)負(fù)載涉及CPU 密集型操作,那么應(yīng)該考慮使用 ProcessPoolExecutor,甚至可以同時(shí)使用這兩種執(zhí)行器,將ProcessPoolExecutor 行器添加為二級(jí)執(zhí)行器。

apscheduler 提供了許多不同的方法來配置調(diào)度器。可以使用字典,也可以使用關(guān)鍵字參數(shù)傳遞。首先實(shí)例化調(diào)度程序,添加作業(yè),然后配置調(diào)度器,獲得最大的靈活性。

如果調(diào)度程序在應(yīng)用程序的后臺(tái)運(yùn)行,選擇 BackgroundScheduler,并使用默認(rèn)的 jobstore 和默認(rèn)的executor,則以下配置即可:

from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()

假如我們想配置更多信息:設(shè)置兩個(gè)執(zhí)行器、兩個(gè)作業(yè)存儲(chǔ)器、調(diào)整新作業(yè)的默認(rèn)值,并設(shè)置不同的時(shí)區(qū)。下述三個(gè)方法是完全等同的。

配置需求
配置名為“mongo”的MongoDBJobStore作業(yè)存儲(chǔ)器
配置名為“default”的SQLAlchemyJobStore(使用SQLite)
配置名為“default”的ThreadPoolExecutor,最大線程數(shù)為20
配置名為“processpool”的ProcessPoolExecutor,最大進(jìn)程數(shù)為5
UTC作為調(diào)度器的時(shí)區(qū)
coalesce默認(rèn)情況下關(guān)閉
作業(yè)的默認(rèn)最大運(yùn)行實(shí)例限制為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
}
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()

以上涵蓋了大多數(shù)情況的調(diào)度器配置,在實(shí)際運(yùn)行時(shí)可以試試不同的配置會(huì)有怎樣不同的效果。

啟動(dòng)調(diào)度器

啟動(dòng)調(diào)度器前需要先添加作業(yè),有兩種方法向調(diào)度器添加作業(yè):一是通過接口add_job(),二是通過使用函數(shù)裝飾器,其中 add_job() 返回一個(gè)apscheduler.job.Job類的實(shí)例,用于后續(xù)修改或刪除作業(yè)。

我們可以隨時(shí)在調(diào)度器上調(diào)度作業(yè)。如果在添加作業(yè)時(shí),調(diào)度器還沒有啟動(dòng),那么任務(wù)將不會(huì)運(yùn)行,并且第一次運(yùn)行時(shí)間在調(diào)度器啟動(dòng)時(shí)計(jì)算。
注意:如果使用的是序列化作業(yè)的執(zhí)行器或作業(yè)存儲(chǔ)器,那么要求被調(diào)用的作業(yè)(函數(shù))必須是全局可訪問的,被調(diào)用的作業(yè)的參數(shù)是可序列化的,作業(yè)存儲(chǔ)器中,只有 MemoryJobStore 不會(huì)序列化作業(yè)。執(zhí)行器中,只有ProcessPoolExecutor 將序列化作業(yè)。

啟用調(diào)度器只需要調(diào)用調(diào)度器的 start() 方法,下面分別使用不同的作業(yè)存儲(chǔ)器來舉例說明:

方法一:使用默認(rèn)的作業(yè)存儲(chǔ)器
# 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()

運(yùn)行結(jié)果如下

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
……

上述代碼使用內(nèi)存作為作業(yè)存儲(chǔ)器,操作比較簡(jiǎn)單,重啟程序相當(dāng)于第一次運(yùn)行。

方法二:使用數(shù)據(jù)庫作為存儲(chǔ)器

# coding:utf-8
from apscheduler.schedulers.blocking import BlockingScheduler
import datetime
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore


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


jobstores = {
    'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
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()

說明,在第 6 行、第 10 行代碼修改為數(shù)據(jù)庫作為作業(yè)存儲(chǔ)器
運(yùn)行結(jié)果如下

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

提示我們有作業(yè)本應(yīng)在 2018-04-05 07:48:05 運(yùn)行的作業(yè)沒有運(yùn)行,因?yàn)楝F(xiàn)在的時(shí)間為 2018-04-05 08:06:34,錯(cuò)過了 0:18:28 的時(shí)間。
如果將上述代碼第 21-25 行注釋掉,重新運(yùn)行本程序,則四種類型的作業(yè)仍會(huì)運(yùn)行,結(jié)果如下:

Run time of job "my_job (trigger: cron[month='4-8,11-12', hour='7-11', second='*/10'], next run at: 2018-04-05 08:14:40 CST)" was missed by 0:00:23.680603
Run time of job "my_job (trigger: cron[month='4-8,11-12', hour='7-11', second='*/10'], next run at: 2018-04-05 08:14:40 CST)" was missed by 0:00:13.681604
Run time of job "my_job (trigger: cron[month='4-8,11-12', hour='7-11', second='*/10'], next run at: 2018-04-05 08:14:40 CST)" was missed by 0:00:03.681604
……
Run time of job "my_job (trigger: interval[0:00:05], next run at: 2018-04-05 08:14:38 CST)" was missed by 0:00:15.687917
Run time of job "my_job (trigger: interval[0:00:05], next run at: 2018-04-05 08:14:38 CST)" was missed by 0:00:10.687917
Run time of job "my_job (trigger: interval[0:00:05], next run at: 2018-04-05 08:14:38 CST)" was missed by 0:00:05.687917
job_interval --> 2018-04-05 08:14:33.821645
job_interval --> 2018-04-05 08:14:38.529167
job_cron --> 2018-04-05 08:14:40.150080
job_interval --> 2018-04-05 08:14:43.296188
job_interval --> 2018-04-05 08:14:48.327317

作業(yè)仍會(huì)運(yùn)行,說明作業(yè)被添加到數(shù)據(jù)庫中,程序中斷后重新運(yùn)行時(shí)會(huì)自動(dòng)從數(shù)據(jù)庫讀取作業(yè)信息,而不需要重新再添加到調(diào)度器中,如果不注釋 21-25 行添加作業(yè)的代碼,則作業(yè)會(huì)重新添加到數(shù)據(jù)庫中,這樣就有了兩個(gè)同樣的作業(yè),避免出現(xiàn)這種情況可以在 add_job 的參數(shù)中增加 replace_existing=True,如

scheduler.add_job(my_job, args=['job_interval',],id='job_interval',trigger='interval',seconds=3,replace_existing=True)

如果我們想運(yùn)行錯(cuò)過運(yùn)行的作業(yè),使用 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,假如一個(gè)作業(yè)本來 08:00 有一次執(zhí)行,但是由于某種原因沒有被調(diào)度上,現(xiàn)在 08:01 了,這個(gè) 08:00 的運(yùn)行實(shí)例被提交時(shí),會(huì)檢查它預(yù)訂運(yùn)行的時(shí)間和當(dāng)下時(shí)間的差值(這里是1分鐘),大于我們?cè)O(shè)置的 30 秒限制,那么這個(gè)運(yùn)行實(shí)例不會(huì)被執(zhí)行。最常見的情形是 scheduler 被 shutdown 后重啟,某個(gè)任務(wù)會(huì)積攢了好幾次沒執(zhí)行如 5 次,下次這個(gè)作業(yè)被提交給執(zhí)行器時(shí),執(zhí)行 5 次。設(shè)置 coalesce=True 后,只會(huì)執(zhí)行一次。
其他操作如下:

scheduler.remove_job(job_id,jobstore=None)#刪除作業(yè)
scheduler.remove_all_jobs(jobstore=None)#刪除所有作業(yè)
scheduler.pause_job(job_id,jobstore=None)#暫停作業(yè)
scheduler.resume_job(job_id,jobstore=None)#恢復(fù)作業(yè)
scheduler.modify_job(job_id, jobstore=None, **changes)#修改單個(gè)作業(yè)屬性信息
scheduler.reschedule_job(job_id, jobstore=None, trigger=None,**trigger_args)#修改單個(gè)作業(yè)的觸發(fā)器并更新下次運(yùn)行時(shí)間
scheduler.print_jobs(jobstore=None, out=sys.stdout)#輸出作業(yè)信息

調(diào)度器事件監(jiān)聽

scheduler 的基本應(yīng)用,在前面已經(jīng)介紹過了,但仔細(xì)思考一下:如果程序有異常拋出會(huì)影響整個(gè)調(diào)度任務(wù)嗎?請(qǐng)看下面的代碼,運(yùn)行一下看看會(huì)發(fā)生什么情況:

# coding:utf-8
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=('定時(shí)任務(wù)',), trigger='cron', second='*/5')

scheduler.start()  

運(yùn)行結(jié)果如下

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 秒拋出一次報(bào)錯(cuò)信息。任何代碼都可能拋出異常,關(guān)鍵是,發(fā)生導(dǎo)常事件,如何第一時(shí)間知道,這才是我們最關(guān)心的,apscheduler 已經(jīng)為我們想到了這些,提供了事件監(jiān)聽來解決這一問題。
將上述代碼稍做調(diào)整,加入日志記錄和事件監(jiān)聽,如下所示。

# coding:utf-8
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 %(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)

def my_listener(event):
    if event.exception:
        print ('任務(wù)出錯(cuò)了!!!!!!')
    else:
        print ('任務(wù)照常運(yùn)行...')

scheduler = BlockingScheduler()
scheduler.add_job(func=date_test, args=('一次性任務(wù),會(huì)出錯(cuò)',), next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=15), id='date_task')
scheduler.add_job(func=aps_test, args=('循環(huán)任務(wù)',), trigger='interval', seconds=3, id='interval_task')
scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
scheduler._logger = logging

scheduler.start()

說明:
第 7-11 行配置日志記錄信息,日志文件在當(dāng)前路徑,文件名為 “l(fā)og1.txt”。
第 33 行啟用 scheduler 模塊的日記記錄。
第 23-27 定義一個(gè)事件監(jiān)聽,出現(xiàn)意外情況打印相關(guān)信息報(bào)警。
運(yùn)行結(jié)果如下所示。

2018-04-05 12:59:29 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:32 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:35 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:38 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:41 一次性任務(wù),會(huì)出錯(cuò)
任務(wù)出錯(cuò)了!!!!!!
2018-04-05 12:59:41 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:44 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...
2018-04-05 12:59:47 循環(huán)任務(wù)
任務(wù)照常運(yùn)行...

在生產(chǎn)環(huán)境中,可以把出錯(cuò)信息換成發(fā)送一封郵件或者發(fā)送一個(gè)短信,這樣定時(shí)任務(wù)出錯(cuò)就可以立馬就知道。

轉(zhuǎn)載于:https://blog.csdn.net/somezz/article/details/83104368

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評(píng)論 1 289
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評(píng)論 2 375