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

jam*_*esh 312 python cron scheduled-tasks

我正在寻找一个Python的库,它将提供atcron功能相似.

我非常喜欢使用纯Python解决方案,而不是依赖于盒子上安装的工具; 这样我就可以在没有cron的机器上运行.

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

 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.
Run Code Online (Sandbox Code Playgroud)

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

如果没有任何东西可以为我开箱即用,那么任何关于构建模块的建议都会感激不尽.

编辑 我对启动进程不感兴趣,只是用Python编写的"作业" - python函数.必要时我认为这将是一个不同的主题,但不是在一个不同的过程中.

为此,我正在寻找cron时间表达式的表现力,但在Python中.

Cron 已经存在多年了,但我试图尽可能地便携.我不能依赖它的存在.

dba*_*der 494

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

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)
Run Code Online (Sandbox Code Playgroud)

披露:我是该图书馆的作者.

  • 有没有办法将参数传递给作业?我想做这样的事情:schedule.every().hour.do(job(myParam)) (21认同)
  • @darrel-holt和@ zen-skunkworx:`do()`函数将传递给它的额外参数转发给作业函数:https://schedule.readthedocs.io/en/stable/api.html#schedule.Job .do例如,你可以这样做:`schedule.every().hour.do(job,param1,param2)`不需要使用lambda.希望这可以帮助 :) (20认同)
  • schedule.every().hour.do(job)每小时运行一次吗?喜欢01:00,02:00,03:00等?即使开始时间不是整整一个小时? (4认同)
  • @jeyanthinath 通常在一个小的无限循环中添加 sleep(1) 甚至 sleep(0.001) 是为了防止 python 使用 100% 的 CPU 核心。 (4认同)
  • 你应该提到你是`schedule`的维护者.它对我很有用.如果它具有类似cron的语法和支持的装饰器会更好(参见crython但不使用此库因为它不起作用;调度似乎写得不好). (3认同)
  • 也许我对此很陌生,但为什么会出现无限循环,这不会让我的线程保持活跃?我应该在单独的线程上运行这个调度程序吗? (3认同)
  • @dbader 向我解释一件事。例如,如果我有类似的功能,我想每 1 分钟打印一些字符串。如果我只是在 `while True:` 循环中执行 `print("xxx")` `time.sleep(60)` 与我将使用您的脚本有什么区别。您使用无限循环,因此它的工作效果不一样? (3认同)
  • @MichaelSchmidt 日程表再次得到积极维护。 (2认同)

Bri*_*ian 64

您可以使用普通的Python参数传递语法来指定您的crontab.例如,假设我们定义一个Event类,如下所示:

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)
Run Code Online (Sandbox Code Playgroud)

(注意:未经过彻底测试)

然后你的CronTab可以用普通的python语法指定为:

c = CronTab(
  Event(perform_backup, 0, 2, dow=6 ),
  Event(purge_temps, 0, range(9,18,2), dow=range(0,5))
)
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以获得Python的参数机制的全部功能(混合位置和关键字args,并且可以使用符号名称来表示数周和数月的名称)

CronTab类将被定义为以微小的增量进行简单的睡眠,并在每个事件上调用check().(可能会有一些细微之处,夏令时/时区要警惕).这是一个快速实现:

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)
Run Code Online (Sandbox Code Playgroud)

需要注意的一点是:Python的工作日/月是零索引(与cron不同),并且该范围排除了最后一个元素,因此像"1-5"这样的语法变为范围(0,5) - 即[0,1,2, 3,4].如果您更喜欢cron语法,那么解析它应该不会太困难.


ssc*_*ssc 46

也许只有在提出问题之后才会出现这种情况; 我想我只是为了完整起见而提到它:https://apscheduler.readthedocs.org/en/latest/


Sea*_*ean 21

在我的搜索中我看到的一件事是python的sched模块,它可能是你正在寻找的那种东西.

  • sched现在有enterabs()进行绝对调度. (9认同)
  • 我希望这是首选的答案,因为sched现在是python2和3 stdlib的一部分,并且可以进行绝对调度。 (3认同)

boo*_*oad 19

"... Crontab模块用于读取和写入crontab文件并自动访问系统cron并简单地使用直接API ...."

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

以及APScheduler,一个python包.已经编写和调试.

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


Duf*_*fau 13

我喜欢pycron包解决这个问题的方式。

import pycron
import time

while True:
    if pycron.is_now('0 2 * * 0'):   # True Every Sunday at 02:00
        print('running backup')
        time.sleep(60)               # The process should take at least 60 sec
                                     # to avoid running twice in one minute
    else:
        time.sleep(15)               # Check again in 15 seconds
Run Code Online (Sandbox Code Playgroud)

  • 这不是一个好主意,因为您的代码“print('running backup')”将以 5 秒的间隔启动整分钟。所以在这种情况下延迟应该是 60 秒。 (2认同)

Hac*_*ron 10

或多或少与上面相同,但使用gevent并发:)

"""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()
Run Code Online (Sandbox Code Playgroud)


rou*_*ble 9

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

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()
Run Code Online (Sandbox Code Playgroud)

帮助程序:

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)
Run Code Online (Sandbox Code Playgroud)


小智 7

我修改了脚本.

  1. 使用方便:

    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()
    
    Run Code Online (Sandbox Code Playgroud)
  2. 尝试在一分钟的第一秒开始任务.

Github上的代码


Nic*_*ick 6

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


小智 6

我对Brian建议CronTab类运行方法有一个小修复.

时间已经过了一秒钟,在每分钟结束时导致一秒钟的硬循环.

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()
Run Code Online (Sandbox Code Playgroud)


Dam*_*tes 6

我知道有很多答案,但另一种解决方案可能是使用decorators。这是每天在特定时间重复函数的示例。使用这种方式很酷的想法是你只需要在你想要调度的函数中添加语法糖

@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
Run Code Online (Sandbox Code Playgroud)

装饰器看起来像:

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()))

            # 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)

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

        wrapperRepeat.nextSent = None
        return wrapperRepeat

    return decoratorRepeat
Run Code Online (Sandbox Code Playgroud)