schedule 是一个广泛使用的 python 定时调度框架,直接上代码。
代码解析
import schedule
import time
def do_task(time_str=time.time()):
print('task run: %s' % time_str)
# 每 10 分钟执行一次任务
schedule.every(10).minutes.do(do_task)
# 每隔 5 到 10 分钟之间的任意一个时间执行一次任务
schedule.every(5).to(10).days.do(do_task)
# 每 1 小时执行一次任务
schedule.every().hour.do(do_task, time_str='1519351479.19554')
# 每天 10:30 执行一次任务
schedule.every().day.at("10:30").do(do_task)
while True:
schedule.run_pending()
time.sleep(1)
schedule 模块的整体设计, 是把任务的自我管理部分做的很详细, 而把上层的调度做的很轻很薄, 关键逻辑点采用回调的方式, 依赖任务的自我管理去实现。
Scheduler 类的实例中, 维护了一个列表: jobs, 专门存储注册进来的任务快照:
Class Scheduler(object):
def __init__(self):
self.jobs = []
Scheduler 类最重要的方法是 run_pending(self), 其主要逻辑是遍历 jobs 列表中的所有 job, 从中找出当前时间点需要调度的 job, 并执行。这其中最重要的逻辑是判断一个 job 当前时间点是否需要被调度, 而这个过程是一个回调, 具体的逻辑则封装在 job.should_run 方法里。
def run_pending(self):
_jobs = (job for job in self.jobs if job.should_run)
for job in sorted(runnable_jobs):
self._run_job(job)
值得注意的是, schedule 模块中并没有专门的逻辑去定时执行 run_pending 方法, 要想让定时调度持续跑起来, 需要自己实现:
while True:
schedule.run_pending()
time.sleep(1)
相比 sched 模块的 ‘伪递归’ 而言, 这样的设计算是比较人性化的了, 可以认为它基本实现了真正意义上的定时调度。 schedule 模块实现了非常详细的任务自我管理逻辑:
class Job(object):
def __init__(self, interval, scheduler=None):
self.interval = interval # pause interval * unit between runs
self.latest = None # upper limit to the interval
self.job_func = None # the job job_func to run
self.unit = None # time units, e.g. 'minutes', 'hours', ...
self.at_time = None # optional time at which this job runs
self.last_run = None # datetime of the last run
self.next_run = None # datetime of the next run
self.period = None # timedelta between runs, only valid for
self.start_day = None # Specific day of the week to start on
self.tags = set() # unique set of tags for the job
self.scheduler = scheduler # scheduler to register with
Job 类的参数众多, 单个任务的调度肯定不可能涉及到所有的参数, 这些参数往往是以局部几个为组合, 控制调度节奏的; 但是无论什么组合, 往往都会以 scheduler.every(interval=1) 方法开始, 以 Job.do(self, job_func, *args, **kwargs) 方法结束。
schedule.every 方法构造出一个 Job 实例, 并设置该实例的第一个参数 interval:
default_scheduler = Scheduler()
def every(interval=1):
return default_scheduler.every(interval)
class Scheduler(object):
def every(self, interval=1):
job = Job(interval, self)
return job
Job.do 方法包装了传递进来的任务函数, 将其设置为自己的 job_func 参数, 并将自己作为一个任务快照放进 scheduler 的任务列表里。
Class Job(object):
def do(self, job_func, *args, **kwargs):
self.job_func = functools.partial(job_func, *args, **kwargs)
try:
functools.update_wrapper(self.job_func, job_func)
except AttributeError:
pass
# 计算下一次调度的时间
self._schedule_next_run()
self.scheduler.jobs.append(self)
return self
在这两个方法之间, 就是通过建造者模式, 构造出其他控制参数的组合, 以实现各种各样的调度节奏。
从关于 Scheduler 类的描述中可以看到, 上层调度中最关键的逻辑, 判断每一个注册的 job 是否应该被调度, 其实是 Job 类的一个回调方法 should_run:
def should_run(self):
return datetime.datetime.now() >= self.next_run
而 should_run 方法中的判断的依据, 是当前时间有没有到达 next_run 这个实例字段给出的时间点; next_run 字段的设置则通过在 Job.do(self, job_func, *args, **kwargs) 方法 (上文已给出) 和 Job.run(self) 方法中调用 __schedule_next_run() 方法来实现:
def run(self):
logger.info('Running job %s', self)
ret = self.job_func()
self.last_run = datetime.datetime.now()
# 计算下一次调度的时间
self._schedule_next_run()
return ret
这些 Job 类的参数组合, 大致可分为这几类:
总基调: 指定调度的周期
以下方法将会设置 unit 参数, 与 interval 参数结合, 定义调度区间间隔:
def second(self): def seconds(self): # self.unit = 'seconds'
def minute(self): def minutes(self): # self.unit = 'minutes'
def hour(self): def hours(self): # self.unit = 'hours'
def day(self): def days(self): # self.unit = 'days'
def week(self): def weeks(self): # self.unit = 'weeks'
其对应的 _schedule_next_run() 逻辑如下(调整前参数):
def _schedule_next_run(self)
assert self.unit in ('seconds', 'minutes', 'hours', 'days', 'weeks')
if self.latest is not None:
assert self.latest >= self.interval
interval = random.randint(self.interval, self.latest)
else:
interval = self.interval
self.period = datetime.timedelta(**{self.unit: interval})
self.next_run = datetime.datetime.now() + self.period
局部调整1: 指定调度的起始 weekday
以下方法将会设置 start_day 参数, 确定调度开始的时间点; 同时统一设置 unit 参数为 ‘weeks’:
def monday(self):
self.start_day = 'monday'
self.unit = 'weeks'
def tuesday(self):
self.start_day = 'tuesday'
self.unit = 'weeks'
def wednesday(self):
self.start_day = 'wednesday'
self.unit = 'weeks'
def thursday(self):
self.start_day = 'thurday'
self.unit = 'weeks'
def friday(self):
self.start_day = 'friday'
self.unit = 'weeks'
def saturday(self):
self.start_day = 'saturday'
self.unit = 'weeks'
def sunday(self):
self.start_day = 'sunday'
self.unit = 'weeks'
其对应的 _schedule_next_run() 逻辑如下,可以发现, start_day 只是在 next_run 原有的 weekday 基础上增加了一个 offset, 相当于是 delay time:
def _schedule_next_run(self):
if self.start_day is not None:
assert self.unit == 'weeks'
weekdays = ('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday')
assert self.start_day in weekdays
weekday = weekdays.index(self.start_day)
days_ahead = weekday - self.next_run.weekday()
if days_ahead <= 0: # Target day already happened this week
days_ahead += 7
self.next_run += datetime.timedelta(days_ahead) - self.period
局部调整2: 指定调度的起始时间
以下方法将会设置 at_time 参数, 其针对 unit == ‘hours’ 只设置 minute 变量, 而对 unit == ‘days’ 或 ‘weeks’ 才会设置 hour 变量:
def at(self, time_str):
assert self.unit in ('days', 'hours') or self.start_day
hour, minute = time_str.split(':')
minute = int(minute)
if self.unit == 'days' or self.start_day:
hour = int(hour)
assert 0 <= hour <= 23
elif self.unit == 'hours':
hour = 0
assert 0 <= minute <= 59
self.at_time = datetime.time(hour, minute)
return self
其对应的 _schedule_next_run() 逻辑也与上面类似, 针对 unit == ‘days’ 或 ‘weeks’ 才设 hour 字段, 否则只设置 minute 和 second;
def _schedule_next_run(self)
if self.at_time is not None:
assert self.unit in ('days', 'hours') or self.start_day is not None
kwargs = {
'minute': self.at_time.minute,
'second': self.at_time.second,
'microsecond': 0
}
if self.unit == 'days' or self.start_day is not None:
kwargs['hour'] = self.at_time.hour
self.next_run = self.next_run.replace(**kwargs)
局部调整3: 在给定范围内随机安排调度时刻
对应的就是上文提及的 latest 参数:
def to(self, latest):
self.latest = latest
return self
具体逻辑就是在给定的 [interval, latest) 区间内, 生成一个随机数作为下次调度的 interval
def _schedule_next_run(self)
if self.latest is not None:
assert self.latest >= self.interval
interval = random.randint(self.interval, self.latest)
else:
interval = self.interval
至此, Job 类的逻辑就都分析完了。
自己写的测试demo:
#coding: utf-8
import schedule
import datetime
import time
def job():
print("作业运行时间:" + str(datetime.datetime.now()))
def finish():
exit(0)
if __name__ == '__main__':
schedule.every().day.at("13:17").do(job)
schedule.every().day.at("13:18").do(finish)
while True:
schedule.run_pending()
time.sleep(1)