Comment obtenir un planificateur de type Cron en Python? [fermé]


348

Je recherche une bibliothèque en Python qui fournira atet cronaimera les fonctionnalités.

J'aimerais tout à fait avoir une solution Python pure, plutôt que de compter sur des outils installés sur la boîte; de cette façon, je fonctionne sur des machines sans cron.

Pour ceux qui ne connaissent pas cron: vous pouvez planifier des tâches basées sur une expression comme:

 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.

La syntaxe de l'expression du temps cron est moins importante, mais j'aimerais avoir quelque chose avec ce genre de flexibilité.

S'il n'y a pas quelque chose qui le fasse pour moi, les suggestions pour les blocs de construction pour faire quelque chose comme ça seraient reçues avec gratitude.

Edit Je ne suis pas intéressé par le lancement de processus, juste des "jobs" également écrits en Python - des fonctions python. Par nécessité, je pense que ce serait un fil différent, mais pas dans un processus différent.

À cette fin, je recherche l'expressivité de l'expression du temps cron, mais en Python.

Cron existe depuis des années, mais j'essaie d'être aussi portable que possible. Je ne peux pas compter sur sa présence.


1
J'aimerais également savoir comment procéder. Il serait plus utile d'avoir une solution multiplateforme que de dépendre de composants spécifiques à la plate-forme.
Sean

7
Ce n'est pas hors sujet, c'est une question très importante et utile
Connor

Réponses:


571

Si vous recherchez un horaire de paiement léger :

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)

Divulgation : je suis l'auteur de cette bibliothèque.


7
Vous devez mentionner que vous êtes le mainteneur de schedule. Ça a bien marché pour moi. Ce serait encore plus agréable s'il avait une syntaxe similaire à cron et des décorateurs pris en charge (voir crython mais n'utilisez pas cette bibliothèque car cela ne fonctionne pas; la planification ne semble pas bien écrite).
Tim Ludwinski

23
Existe-t-il un moyen de transmettre un paramètre au travail? Je voudrais faire quelque chose comme ceci: schedule.every (). Hour.do (job (myParam))
Zen Skunkworx

5
schedule.every (). hour.do (job) Est-ce que cela fonctionne toutes les heures d'horloge? Comme 01h00, 02h00, 03h00, etc.? même si l'heure de début n'est pas une heure complète?
swateek

1
supposons que ce code se trouve dans scheduler.py. ce code s'exécutera-t-il automatiquement?
Kishan

25
@ darrel-holt et @ zen-skunkworx: La do()fonction transfère les arguments supplémentaires que vous lui passez à la fonction de travail: schedule.readthedocs.io/en/stable/api.html#schedule.Job.do Par exemple, vous pouvez le faire : schedule.every().hour.do(job, param1, param2)Pas besoin d'utiliser un lambda. J'espère que cela aide :)
dbader

65

Vous pouvez simplement utiliser la syntaxe de passage d'argument Python normale pour spécifier votre crontab. Par exemple, supposons que nous définissions une classe d'événements comme ci-dessous:

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)

(Remarque: non testé à fond)

Ensuite, votre CronTab peut être spécifié dans la syntaxe python normale comme:

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

De cette façon, vous obtenez toute la puissance de la mécanique d'argument de Python (mélange d'arguments positionnels et de mots clés, et pouvez utiliser des noms symboliques pour des noms de semaines et de mois)

La classe CronTab serait définie comme dormant simplement par incréments d'une minute et appelant check () à chaque événement. (Il faut probablement se méfier de certaines subtilités avec l'heure d'été / les fuseaux horaires). Voici une mise en œuvre rapide:

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)

Quelques choses à noter: les jours de semaine / mois de Python sont indexés zéro (contrairement à cron), et cette plage exclut le dernier élément, donc la syntaxe comme "1-5" devient plage (0,5) - c'est-à-dire [0,1,2, 3,4]. Si vous préférez la syntaxe cron, l'analyse ne devrait cependant pas être trop difficile.


Vous voudrez peut-être ajouter des instructions d'importation pour les inexpérimentés. J'ai fini par mettre toutes les classes dans un seul fichier avec from datetime import * from time import sleep et changé time.sleep en sleep. Belle solution simple et élégante. Merci.
mavnn

1
Je me demande simplement pourquoi cela est préféré à Kronos? Sched est-il bogué (puisque kronos utilise sched)? Ou est-ce juste obsolète?
cregox

Merci brian, j'utilise votre solution en production et ça marche plutôt bien. Cependant, comme d'autres l'ont souligné, il existe un bug subtil dans votre code d'exécution. De plus, je l'ai trouvé trop compliqué pour les besoins.
raph.amiard

1
C'est cool, mais ne prend toujours pas en charge la notation slash, pour une exécution toutes les heures, min, etc ...
Chris Koston

1
Excellente idée d'écrire vos propres cours, par exemple lorsque je n'ai pas accès à sudo sur un serveur et que je ne peux donc pas pip install anything:)
Cometsong


27

Une chose que j'ai vue dans mes recherches est le schedmodule de python qui pourrait être le genre de chose que vous recherchez.


11
sched a maintenant enterabs () qui fait une programmation absolue.
Jerther

5
Je m'attendrais à ce que ce soit la réponse préférée car sched fait partie de python2 et 3 stdlib maintenant et fait une programmation absolue.
Michael


11

Plus ou moins les mêmes que ci-dessus mais en même temps que 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()

Juste une note que datetime.timetuple () commencera par l'année, le mois, le jour ... etc ...
Trey Stout

9

Aucune des solutions répertoriées ne tente même d'analyser une chaîne de planification cron complexe. Alors, voici ma version, en utilisant croniter . Contenu de base:

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

Routines d'assistance:

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)

Comment quelqu'un pourrait-il participer à "une exécution manquée" elif? Atm J'utilise un programme comme en "* * * * *"ajoutant ensuite time.sleepplus de 1 minute dans le "Faites votre truc périodique" if, mais je vois toujours le truc dans cette instruction if. Quand cela prend plus d'une minute, je vois juste la boucle while sauter cette exécution de boucle manquante.
TPPZ

@TPPZ Le processus aurait pu être suspendu, l'horloge aurait pu être modifiée manuellement ou par ntp etc. etc. Croniter est utilisé dans Airflow et il semble plus complet que le module Crontab et autres.
dlamblin

7

J'ai modifié le script.

  1. Facile à utiliser:

    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()
  2. Essayez de démarrer la tâche dans la première seconde d'une minute.

Code sur Github


6

J'ai un correctif mineur pour la méthode d'exécution de classe CronTab suggérée par Brian .

Le chronométrage a été interrompu d'une seconde, ce qui a entraîné une boucle dure d'une seconde à la fin de chaque minute.

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

4

Il n'y a pas de méthode "pure python" pour cela car un autre processus devrait lancer python pour exécuter votre solution. Chaque plateforme aura une ou vingt façons différentes de lancer des processus et de suivre leur progression. Sur les plates-formes Unix, cron est l'ancien standard. Sur Mac OS X, il existe également launchd, qui combine un lancement de type cron avec une fonctionnalité de surveillance qui peut maintenir votre processus en vie si c'est ce que vous voulez. Une fois que python est en cours d'exécution, vous pouvez utiliser le module sched pour planifier des tâches.


4

Je sais qu'il y a beaucoup de réponses, mais une autre solution pourrait être d'aller avec des décorateurs . Ceci est un exemple pour répéter une fonction tous les jours à une heure précise. La bonne idée d'utiliser cette méthode est que vous n'avez qu'à ajouter le sucre syntaxique à la fonction que vous souhaitez planifier:

@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

Et le décorateur ressemblera à:

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

1

La solution de Brian fonctionne assez bien. Cependant, comme d'autres l'ont souligné, il y a un bug subtil dans le code d'exécution. De plus, je l'ai trouvé trop compliqué pour les besoins.

Voici mon alternative plus simple et fonctionnelle pour le code d'exécution au cas où quelqu'un en aurait besoin:

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

Une autre solution triviale serait:

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)

Et la classe aqcron.At est:

# 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

1
Soyez prudent lorsque vous publiez des réponses copier-coller passe-partout / verbatim à plusieurs questions, celles-ci ont tendance à être signalées comme "spam" par la communauté. Si vous faites cela, cela signifie généralement que les questions sont en double, alors signalez-les à la place: stackoverflow.com/a/12360556/419
Kev

1

Si vous recherchez un planificateur distribué, vous pouvez consulter https://github.com/sherinkurian/mani - il a besoin de redis mais ce n'est peut-être pas ce que vous recherchez. (notez que je suis l'auteur) ceci a été construit pour assurer la tolérance aux pannes en faisant fonctionner l'horloge sur plusieurs nœuds.


0

Je ne sais pas si quelque chose comme ça existe déjà. Il serait facile d'écrire le vôtre avec des modules d'heure, de date et / ou de calendrier, voir http://docs.python.org/library/time.html

La seule préoccupation pour une solution python est que vos besoins d'emploi d'être toujours en cours d' exécution et peut - être automatiquement « ressuscités » après un redémarrage, quelque chose pour lequel vous avez besoin de compter sur des solutions dépendantes du système.


3
Rouler le vôtre est une option - bien que le meilleur code soit le code que vous n'avez pas à écrire. La résurrection, je suppose, est quelque chose que je devrais peut-être considérer.
jamesh


0

Méthode de Crontab sur le serveur.

Nom de fichier Python hello.py

Étape 1: créer un fichier sh et donner le nom à s.sh

python3 /home/ubuntu/Shaurya/Folder/hello.py> /home/ubuntu/Shaurya/Folder/log.txt 2> & 1

Étape 2: Ouvrez l'éditeur Crontab

crontab -e

Étape 3: ajouter une heure de planification

Utiliser la mise en forme Crontab

2 * * * * sudo sh /home/ubuntu/Shaurya/Folder/s.sh

Ce cron s'exécutera «à la minute 2».


0

J'aime la façon dont le paquet pycron résout ce problème.

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)

1
Ce n'est pas une bonne idée, car votre code "print ('running backup')" lancera une minute entière avec un intervalle de 5 secondes. Dans ce cas, le délai devrait donc être de 60 secondes.
n158

Tu as raison! Merci d'avoir fait remarquer cela.
Duffau
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.