一个简单的定时任务框架
在生产中经常遇到一些问题,比如我有很多任务,每个任务都有固定的执行周期,比如一天一次,或者一小时一次,执行后我需要将执行结果以邮件的方式发给我。在可预期的将来,还会有这种任务出现,并且加入到这些任务的队列中。
显然,这些任务毫不相干,但都需要定时运行,由于我目前的任务都是python任务,一个一个的启动管理过于麻烦,而合并所有任务对于调试、后期添加任务都是一个负担。于是有了这个小框架,将其称为框架也算是过分的夸赞它了
这个框架有一个好处,那就是一次启动,随时添加、删除任务
主程序
主程序要干的就三件事
- 从配置文件中获取任务,并添加或删除任务
- 定点、定时执行任务,并且接收任务完成后的返回值
- 将返回值用邮件发送给特定的邮箱
基于以上三个任务,需要用到这三个python库
apscheduler
watchdog
email
这几个库的具体用法可以自行百度,我这里只说说我用到的
监听
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
class Timing(FileSystemEventHandler):
def __init__(self):
super(Timing, self).__init__()
# 任务加载相关
self._watch_path = "Task/task.json"
def on_closed(self, event):
pass
这里创建一个类,继承FileSystemEventHandler
,这个类可以用来监听类属性self._watch_path
规定的目录或文件,我这里监听的是一个文件,注意,这是类属性,别写错了。
父类FileSystemEventHandler
中定义了几种方法对应监听到不同情况下的方法,如下
self.on_any_event(event)
{
EVENT_TYPE_CREATED: self.on_created,
EVENT_TYPE_DELETED: self.on_deleted,
EVENT_TYPE_MODIFIED: self.on_modified,
EVENT_TYPE_MOVED: self.on_moved,
EVENT_TYPE_CLOSED: self.on_closed,
}[event.event_type](event)
我这里只用到了on_closed
,在文件修改后会触发一次,后续我们启动主方法后,就可以通过修改文件动态的加载任务,而不用反复启动停止主方法
动态加载任务
加载任务就需要用到apscheduler
了,我愿称它为python最强定时任务
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
# 定时任务相关
self.scheduler = BlockingScheduler()
通过self.scheduler
就可以添加一个定时任务
self.scheduler.add_job(func1, 'cron', hour='0-23', minute="30")
这里表示0:30,1:30,2:30......23:30
都会执行func1
,如果需要每分钟执行一次可以设置为*/1
我们可以通过jobs = self.scheduler.get_jobs()
获取当前的所有任务,返回值为一个列表,可通过如下方法获取方法的名称和ID
for job in jobs:
print(job.name)
print(job.id)
任务的名称和代码中的方法名相同
获取到ID后就可以删除一个任务
self.scheduler.remove_job(jog_id)
我们通过读取配置文件,去添加或删除任务,将配置文件中新增的加入任务队列,消失的从队列中删除
以下代码仅供参考,可根据不同的任务去写不同结构的配置文件
{
"PringA": {
"Path": "Task/Sample_task.py",
"Job": "PringA",
"JobTime": {
"hour": "0-23",
"minute": "*/1"
},
"mail": {
}
},
"PrintB": {
"Path": "Task/Sample_task.py",
"Job": "PrintB",
"JobTime": {
"hour": "0-23",
"minute": "*/1"
},
"mail": {
}
}
}
def load_task(self):
if os.path.exists(self._watch_path):
with open(self._watch_path, 'r', encoding='UTF-8') as f:
load_dict = json.load(f)
load_job_name = []
for job in load_dict:
job = load_dict[job]
job_func = job['Job']
job_name = job_func
job_path = job['Path']
job_time_hour = job['JobTime']['hour']
job_time_minute = job['JobTime']['minute']
load_job_name.append(job_name)
if job_name not in self.job_dict.keys():
job_import = "from {} import {}".format(".".join(job_path.replace(".py", "").split("/")), job_func)
exec(job_import)
self.scheduler.add_job(eval(job_func), 'cron', hour=job_time_hour, minute=job_time_minute)
self.log.info("{} add job {}".format(job_import, job_func))
for add_job_name in self.job_dict.keys():
if add_job_name == "Timing.print_time":
continue
if add_job_name not in load_job_name:
add_jog_id = self.job_dict[add_job_name]['id']
self.scheduler.remove_job(add_jog_id)
self.log.info("remove job {}, id={}".format(add_job_name, add_jog_id))
self.update_job_dict()
return load_dict
else:
self.log.info("{} not exists".format(self._watch_path))
接收返回值
当一个定时任务完成时,可以通过一个监听器去获取它的返回值
self.scheduler.add_listener(函数, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
在监听到任务完成返回一个值后,就会调用配置的函数去处理,在这里,我选择接收到返回值后就发送邮件
发送邮件
def send_mail(self, event):
job_name = self.job_dict_id[event.job_id]
if self.task_dict is None:
return
if job_name not in self.task_dict.keys():
return
if 'mail' not in self.task_dict[job_name]:
return
mail_receivers = self.task_dict[job_name]['mail']['receivers'] if "receivers" in self.task_dict[job_name]['mail'] else self.mail_default_debug
mail_debug = self.task_dict[job_name]['mail']['debug'] if "debug" in self.task_dict[job_name]['mail'] else self.mail_default_debug
mail_msg = self.task_dict[job_name]['mail']['msg'] if "msg" in self.task_dict[job_name]['mail'] else self.mail_default_msg
mail_subject = self.task_dict[job_name]['mail']['subject'] if "subject" in self.task_dict[job_name]['mail'] else self.mail_default_subject
message = MIMEMultipart()
message.attach(MIMEText(mail_msg, 'html', 'utf-8'))
message['From'] = Header("Timing定时任务<{}>".format(self.mail_user), 'utf-8') # 发送者
message['To'] = Header("timing<timing@placeholder.com>", 'utf-8') # 接收者
job_return = event.retval
send_success = False
if job_return:
file_path, file_len = job_return
if os.path.exists(file_path):
message['Subject'] = Header("{} {} 获取数据{}条".format(mail_subject, datetime.now().strftime(self.date_style), file_len), 'utf-8')
msg_xlsx = MIMEApplication(open(file_path, 'rb').read())
msg_xlsx.add_header('Content-Disposition', 'attachment', filename=os.path.basename(file_path))
message.attach(msg_xlsx)
try:
smtpObj = smtplib.SMTP_SSL(self.mail_host, self.mail_port)
smtpObj.login(self.mail_user, self.mail_pass)
smtpObj.sendmail(self.mail_sender, mail_receivers, message.as_string())
self.log.info("邮件发送成功 {} to {}".format(self.mail_sender, ", ".join(mail_receivers)))
smtpObj.quit()
send_success = True
except smtplib.SMTPException as e:
self.log.error("无法发送邮件 {}".format(e))
message['Subject'] = Header("{} {} 发送邮件失败 {}".format(mail_subject, datetime.now().strftime(self.date_style), e), 'utf-8')
else:
message['Subject'] = Header("{} {} {}文件不存在".format(mail_subject, datetime.now().strftime(self.date_style), file_path), 'utf-8')
else:
message['Subject'] = Header("{} {} 任务无返回值".format(mail_subject, datetime.now().strftime(self.date_style)), 'utf-8')
if not send_success:
try:
smtpObj = smtplib.SMTP_SSL(self.mail_host, self.mail_port)
smtpObj.login(self.mail_user, self.mail_pass)
smtpObj.sendmail(self.mail_sender, mail_debug, message.as_string())
self.log.info("debug邮件发送成功 {} to {}".format(self.mail_sender, ", ".join(mail_receivers)))
smtpObj.quit()
except smtplib.SMTPException as e:
self.log.error("debug邮件无法发送 {}".format(e))
def run(self):
self.scheduler.start()