如何在Python中获得类似Cron的调度程序?

How do I get a Cron like scheduler in Python?

我正在寻找一个用python编写的库,它将提供类似于atcron的功能。

我非常希望有一个纯粹的Python解决方案,而不是依赖安装在盒子上的工具;这样我就可以在没有cron的机器上运行。

对于不熟悉cron的人:您可以根据如下表达式安排任务:

1
2
 0 2 * * 7 /usr/bin/run-backup # run the backups at 0200 on Every Sunday
 0 9-17/2 * * 1-5 /usr/bin/purge-temps # run the purge temps command, every 2 hours between 9am and 5pm on Mondays to Fridays.

cron时间表达式语法不那么重要,但我希望有这种灵活性。

如果没有什么东西对我来说是开箱即用的,那么对于构建块做出这样的东西的任何建议都将得到感激。

编辑我对启动进程不感兴趣,只是"jobs"也用python-python函数编写。必要时,我认为这是一条不同的线,但不是在一个不同的过程中。

为此,我在寻找cron-time表达式的表现力,但在python中。

Cron已经存在多年了,但我正在尽可能的便携。我不能依靠它的存在。


如果您正在寻找轻量级的结账时间表:

1
2
3
4
5
6
7
8
9
10
11
12
13
import schedule
import time

def job():
    print("I'm working...")

schedule.every(10).minutes.do(job)
schedule.every().hour.do(job)
schedule.every().day.at("10:30").do(job)

while 1:
    schedule.run_pending()
    time.sleep(1)

披露:我是那个图书馆的作者。


您可以使用普通的python参数传递语法来指定crontab。例如,假设我们定义一个事件类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from datetime import datetime, timedelta
import time

# Some utility classes / functions first
class AllMatch(set):
   """Universal set - match everything"""
    def __contains__(self, item): return True

allMatch = AllMatch()

def conv_to_set(obj):  # Allow single integer to be provided
    if isinstance(obj, (int,long)):
        return set([obj])  # Single item
    if not isinstance(obj, set):
        obj = set(obj)
    return obj

# The actual Event class
class Event(object):
    def __init__(self, action, min=allMatch, hour=allMatch,
                       day=allMatch, month=allMatch, dow=allMatch,
                       args=(), kwargs={}):
        self.mins = conv_to_set(min)
        self.hours= conv_to_set(hour)
        self.days = conv_to_set(day)
        self.months = conv_to_set(month)
        self.dow = conv_to_set(dow)
        self.action = action
        self.args = args
        self.kwargs = kwargs

    def matchtime(self, t):
       """Return True if this event should trigger at the specified datetime"""
        return ((t.minute     in self.mins) and
                (t.hour       in self.hours) and
                (t.day        in self.days) and
                (t.month      in self.months) and
                (t.weekday()  in self.dow))

    def check(self, t):
        if self.matchtime(t):
            self.action(*self.args, **self.kwargs)

(注:未彻底测试)

然后可以用普通的python语法将crontab指定为:

1
2
3
4
c = CronTab(
  Event(perform_backup, 0, 2, dow=6 ),
  Event(purge_temps, 0, range(9,18,2), dow=range(0,5))
)

这样,您就可以充分利用Python的参数机制(混合位置参数和关键字参数,并可以使用符号名称表示周和月的名称)。

crontab类将被定义为简单地以分钟为增量睡眠,并对每个事件调用check()。(不过,夏令时/时区可能有一些微妙之处需要注意)。下面是一个快速实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CronTab(object):
    def __init__(self, *events):
        self.events = events

    def run(self):
        t=datetime(*datetime.now().timetuple()[:5])
        while 1:
            for e in self.events:
                e.check(t)

            t += timedelta(minutes=1)
            while datetime.now() < t:
                time.sleep((t - datetime.now()).seconds)

需要注意的几点是:python的工作日/月是零索引的(与cron不同),并且该范围不包括最后一个元素,因此类似"1-5"的语法变成了range(0,5)-即[0,1,2,3,4]。如果您喜欢cron语法,那么解析它不应该太困难。


也许这个问题是在被问到之后才出现的;我想我只是为了完整起见才提到它:https://apscheduler.readthedocs.org/en/latest/


看看芹菜,它们有像cron这样的周期性任务。


"……"crontab模块,用于读取和写入crontab文件,并使用直接API自动访问系统cron。……"

http://pypi.python.org/pypi/python-crontab

还有apscheduler,一个python包。已写入并调试。

http://packages.python.org/apscheduler/cronschedule.html


在我的搜索中,我看到的一件事是python的sched模块,这可能是您正在寻找的类型。


或多或少与上述相同,但同时使用gevent:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""Gevent based crontab implementation"""

from datetime import datetime, timedelta
import gevent

# Some utility classes / functions first
def conv_to_set(obj):
   """Converts to set allowing single integer to be provided"""

    if isinstance(obj, (int, long)):
        return set([obj])  # Single item
    if not isinstance(obj, set):
        obj = set(obj)
    return obj

class AllMatch(set):
   """Universal set - match everything"""
    def __contains__(self, item):
        return True

allMatch = AllMatch()

class Event(object):
   """The Actual Event Class"""

    def __init__(self, action, minute=allMatch, hour=allMatch,
                       day=allMatch, month=allMatch, daysofweek=allMatch,
                       args=(), kwargs={}):
        self.mins = conv_to_set(minute)
        self.hours = conv_to_set(hour)
        self.days = conv_to_set(day)
        self.months = conv_to_set(month)
        self.daysofweek = conv_to_set(daysofweek)
        self.action = action
        self.args = args
        self.kwargs = kwargs

    def matchtime(self, t1):
       """Return True if this event should trigger at the specified datetime"""
        return ((t1.minute     in self.mins) and
                (t1.hour       in self.hours) and
                (t1.day        in self.days) and
                (t1.month      in self.months) and
                (t1.weekday()  in self.daysofweek))

    def check(self, t):
       """Check and run action if needed"""

        if self.matchtime(t):
            self.action(*self.args, **self.kwargs)

class CronTab(object):
   """The crontab implementation"""

    def __init__(self, *events):
        self.events = events

    def _check(self):
       """Check all events in separate greenlets"""

        t1 = datetime(*datetime.now().timetuple()[:5])
        for event in self.events:
            gevent.spawn(event.check, t1)

        t1 += timedelta(minutes=1)
        s1 = (t1 - datetime.now()).seconds + 1
        print"Checking again in %s seconds" % s1
        job = gevent.spawn_later(s1, self._check)

    def run(self):
       """Run the cron forever"""

        self._check()
        while True:
            gevent.sleep(60)

import os
def test_task():
   """Just an example that sends a bell and asd to all terminals"""

    os.system('echo asd | wall')  

cron = CronTab(
  Event(test_task, 22, 1 ),
  Event(test_task, 0, range(9,18,2), daysofweek=range(0,5)),
)
cron.run()


基于Kronos的具有预定任务能力的涡轮齿轮船

我从来没有直接使用过Kronos,但是TG中的调度有一套很好的特性,而且是可靠的。


所有列出的解决方案都没有尝试解析复杂的cron调度字符串。所以,这里是我的版本,使用croniter。基本要旨:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
schedule ="*/5 * * * *" # Run every five minutes

nextRunTime = getNextCronRunTime(schedule)
while True:
     roundedDownTime = roundDownTime()
     if (roundedDownTime == nextRunTime):
         ####################################
         ### Do your periodic thing here. ###
         ####################################
         nextRunTime = getNextCronRunTime(schedule)
     elif (roundedDownTime > nextRunTime):
         # We missed an execution. Error. Re initialize.
         nextRunTime = getNextCronRunTime(schedule)
     sleepTillTopOfNextMinute()

帮助程序例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from croniter import croniter
from datetime import datetime, timedelta

# Round time down to the top of the previous minute
def roundDownTime(dt=None, dateDelta=timedelta(minutes=1)):
    roundTo = dateDelta.total_seconds()
    if dt == None : dt = datetime.now()
    seconds = (dt - dt.min).seconds
    rounding = (seconds+roundTo/2) // roundTo * roundTo
    return dt + timedelta(0,rounding-seconds,-dt.microsecond)

# Get next run time from now, based on schedule specified by cron string
def getNextCronRunTime(schedule):
    return croniter(schedule, datetime.now()).get_next(datetime)

# Sleep till the top of the next minute
def sleepTillTopOfNextMinute():
    t = datetime.utcnow()
    sleeptime = 60 - (t.second + t.microsecond/1000000.0)
    time.sleep(sleeptime)


我已经修改了脚本。

  • 易于使用:

    1
    2
    3
    4
    5
    cron = Cron()
    cron.add('* * * * *'   , minute_task) # every minute
    cron.add('33 * * * *'  , day_task)    # every hour
    cron.add('34 18 * * *' , day_task)    # every day
    cron.run()
  • 尝试在一分钟的第一秒钟内开始任务。

  • GITHUB代码


    查看luigi(https://github.com/spotify/luigi)。它是用python编写的,有一个很好的用于监视任务的Web UI。它也有一个依赖关系图。可能会因为你需要的东西而被过度杀戮,但它可能会起作用。


    我对Brian建议的crontab类run方法有一个小修复。

    计时结束了一秒钟,进入了一秒钟,每分钟结束时都会有一个硬循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class CronTab(object):
        def __init__(self, *events):
            self.events = events

        def run(self):
            t=datetime(*datetime.now().timetuple()[:5])
            while 1:
                for e in self.events:
                    e.check(t)

                t += timedelta(minutes=1)
                n = datetime.now()
                while n < t:
                    s = (t - n).seconds + 1
                    time.sleep(s)
                    n = datetime.now()

    没有一种"纯Python"方法可以做到这一点,因为其他一些进程必须启动Python才能运行您的解决方案。每个平台将有一种或二十种不同的方法来启动流程并监控其进度。在UNIX平台上,cron是旧标准。在MacOSX上还有launchd,它结合了类似cron的启动和看门狗的功能,如果你想要的话,可以让你的进程保持活动。一旦python运行了,就可以使用sched模块来调度任务。


    以防万一,如果您使用的是Windows,就存在一个pycron。查看http://sourceforge.net/projects/pycron/。对于Linux,我将使用cron或sched。


    布赖恩的解决方案很有效。然而,正如其他人指出的,运行代码中有一个微妙的错误。而且我发现它对于需求来说过于复杂。

    以下是我对运行代码的简单和实用的替代方案,以防有人需要它:

    1
    2
    3
    4
    5
    6
    7
    def run(self):
        while 1:
            t = datetime.now()
            for e in self.events:
                e.check(t)

            time.sleep(60 - t.second - t.microsecond / 1000000.0)

    另一个简单的解决方案是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from aqcron import At
    from time import sleep
    from datetime import datetime

    # Event scheduling
    event_1 = At( second=5 )
    event_2 = At( second=[0,20,40] )

    while True:
        now = datetime.now()

        # Event check
        if now in event_1: print"event_1"
        if now in event_2: print"event_2"

        sleep(1)

    aqcron.at类是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # aqcron.py

    class At(object):
        def __init__(self, year=None,    month=None,
                     day=None,     weekday=None,
                     hour=None,    minute=None,
                     second=None):
            loc = locals()
            loc.pop("self")
            self.at = dict((k, v) for k, v in loc.iteritems() if v != None)

        def __contains__(self, now):
            for k in self.at.keys():
                try:
                    if not getattr(now, k) in self.at[k]: return False
                except TypeError:
                    if self.at[k] != getattr(now, k): return False
            return True


    如果您正在寻找一个分布式调度程序,您可以查看https://github.com/sherinkurian/mani-它确实需要redis,所以可能不是您要找的。(注意我是作者)这是为了通过在多个节点上运行时钟来确保容错性而构建的。


    我不知道这样的东西是否已经存在。使用时间、日期时间和/或日历模块很容易编写自己的文档,请参见http://docs.python.org/library/time.html。

    对Python解决方案唯一的关注是,您的作业需要始终运行,并且可能在重新启动后自动"恢复",对于这种情况,您确实需要依赖于系统相关的解决方案。


    我知道有很多答案,但另一个解决办法可能是和装饰师一起。这是一个在特定时间每天重复一个函数的例子。使用这种方式的一个很酷的想法是,您只需要向您想要调度的函数添加语法糖分:

    1
    2
    3
    4
    5
    @repeatEveryDay(hour=6, minutes=30)
    def sayHello(name):
        print(f"Hello {name}")

    sayHello("Bob") # Now this function will be invoked every day at 6.30 a.m

    装饰师看起来像:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    def repeatEveryDay(hour, minutes=0, seconds=0):
       """
        Decorator that will run the decorated function everyday at that hour, minutes and seconds.
        :param hour: 0-24
        :param minutes: 0-60 (Optional)
        :param seconds: 0-60 (Optional)
       """

        def decoratorRepeat(func):

            @functools.wraps(func)
            def wrapperRepeat(*args, **kwargs):

                def getLocalTime():
                    return datetime.datetime.fromtimestamp(time.mktime(time.localtime()))

                #&nbsp;Get the datetime of the first function call
                td = datetime.timedelta(seconds=15)
                if wrapperRepeat.nextSent == None:
                    now = getLocalTime()
                    wrapperRepeat.nextSent = datetime.datetime(now.year, now.month, now.day, hour, minutes, seconds)
                    if wrapperRepeat.nextSent < now:
                        wrapperRepeat.nextSent += td

                # Waiting till next day
                while getLocalTime() < wrapperRepeat.nextSent:
                    time.sleep(1)

                # Call the function
                func(*args, **kwargs)

                #&nbsp;Get the datetime of the next function call
                wrapperRepeat.nextSent += td
                wrapperRepeat(*args, **kwargs)

            wrapperRepeat.nextSent = None
            return wrapperRepeat

        return decoratorRepeat


    您可以查看picloud的[1]crons[2],但请注意,您的作业不会在您自己的计算机上运行。如果你一个月使用超过20小时的计算时间,这也是一项你需要支付的服务。

    [1]网址:http://www.picloud.com

    [2]网址:http://docs.picloud.com/cron.html


    如果要运行的脚本基于Web,可以考虑使用第三方服务(如crono)以编程方式设置作业。


    我采用了布莱恩的解决方案,做了一些修改,添加了标准crontab文件解析器的开头,并将其放在https://bitback.org/dbename/devcron上。