Come posso ottenere un cronografo come lo scheduler in Python? [chiuso]


348

Sto cercando una libreria in Python che fornirà ate croncome funzionalità.

Mi piacerebbe piuttosto avere una soluzione Python pura, piuttosto che fare affidamento su strumenti installati sulla scatola; in questo modo corro su macchine senza cron.

Per chi non ha familiarità con cron: è possibile pianificare attività basate su un'espressione come:

 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 sintassi dell'espressione cron time è meno importante, ma vorrei avere qualcosa con questo tipo di flessibilità.

Se non c'è qualcosa che lo faccia per me, qualsiasi suggerimento per i blocchi di costruzione per fare qualcosa del genere verrebbe ricevuto con gratitudine.

Modifica Non mi interessa avviare processi, solo "lavori" scritti anche in Python - funzioni di Python. Per necessità, penso che questo sarebbe un filo diverso, ma non in un processo diverso.

A tal fine, sto cercando l'espressività dell'espressione cron time, ma in Python.

Cron è in circolazione da anni, ma sto cercando di essere il più portatile possibile. Non posso fare affidamento sulla sua presenza.


1
Vorrei anche sapere come fare. Sarebbe più utile disporre di una soluzione multipiattaforma piuttosto che dipendere da componenti specifici della piattaforma.
Sean,

7
Questo non è fuori tema, questa è una domanda molto importante e utile
Connor

Risposte:


572

Se stai cercando un programma di pagamento leggero :

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)

Divulgazione : sono l'autore di quella biblioteca.


8
Dovresti dire che sei il manutentore di schedule. Ha funzionato bene nel mio caso. Sarebbe ancora più bello se avesse cron come sintassi e decoratori supportati (vedi crython ma non usi questa libreria perché non funziona; la pianificazione non sembra essere scritta bene).
Tim Ludwinski l'

23
C'è un modo per passare un parametro al lavoro? Mi piacerebbe fare qualcosa del genere: schedule.every (). Hour.do (lavoro (myParam))
Zen Skunkworx,

5
schedule.every (). hour.do (lavoro) viene eseguito ogni ora di clock? Come 01:00, 02:00, 03:00, ecc.? anche se l'ora di inizio non è un'ora intera?
Swateek,

1
supponiamo che questo codice sia in scheduler.py. questo codice verrà eseguito automaticamente?
Kishan,

26
@ darrel-holt e @ zen-skunkworx: la do()funzione inoltra argomenti extra che le si passano alla funzione lavoro: schedule.readthedocs.io/en/stable/api.html#schedule.Job.do Ad esempio, è possibile farlo : schedule.every().hour.do(job, param1, param2)Non è necessario utilizzare una lambda. Spero che questo aiuti :)
Dbader,

65

Puoi semplicemente usare il normale argomento Python passando la sintassi per specificare il tuo crontab. Ad esempio, supponiamo di definire una classe Event come di seguito:

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)

(Nota: non accuratamente testato)

Quindi il tuo CronTab può essere specificato nella normale sintassi di Python come:

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

In questo modo ottieni tutta la potenza della meccanica degli argomenti di Python (mescolando argomenti posizionali e parole chiave, e puoi usare nomi simbolici per nomi di settimane e mesi)

La classe CronTab sarebbe definita semplicemente dormendo in incrementi di minuto e chiamando check () su ciascun evento. (Ci sono probabilmente alcune sottigliezze con l'ora legale / fusi orari di cui fare attenzione però). Ecco una rapida implementazione:

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)

Alcune cose da notare: i giorni della settimana / i mesi di Python sono indicizzati a zero (a differenza di cron) e quell'intervallo esclude l'ultimo elemento, quindi la sintassi come "1-5" diventa intervallo (0,5) - vale a dire [0,1,2, 3,4]. Se preferisci la sintassi cron, analizzarla non dovrebbe essere troppo difficile.


Potresti voler aggiungere alcune dichiarazioni di importazione per gli inesperti. Ho finito per mettere tutte le classi in un singolo file con dall'importazione datetime * dal time import sleep e ho cambiato time.sleep in sleep. Bella, semplice soluzione elegante. Grazie.
mavnn,

1
Mi chiedo solo, perché questo viene preferito a Kronos? Sched è buggy (dal momento che kronos usa sched)? O è solo obsoleto?
Cregox,

Grazie Brian, uso la tua soluzione in produzione e funziona abbastanza bene. Tuttavia, come altri hanno sottolineato, c'è un bug sottile nel codice di esecuzione. Inoltre l'ho trovato eccessivamente complicato per le esigenze.
raph.amiard

1
Questo è bello, ma non supporta ancora la notazione della barra, per l'esecuzione ogni ora, min, ecc ...
Chris Koston,

1
Ottima idea di scrivere le tue lezioni, ad esempio quando non ho accesso sudo su un server e quindi non posso pip install anything:)
Cometsong


27

Una cosa che ho visto nelle mie ricerche è il schedmodulo di Python che potrebbe essere il tipo di cosa che stai cercando.


11
sched ora ha enterabs () che esegue una pianificazione assoluta.
Jerther,

5
Mi aspetterei che questa sia la risposta preferita poiché sched fa parte di Python2 e 3 stdlib ora e fa la pianificazione assoluta.
Michael,


11

Più o meno come sopra ma simultaneamente usando 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()

Solo una nota che datetime.timetuple () inizierà con anno, mese, giorno ... ecc ...
Trey Stout,

9

Nessuna delle soluzioni elencate tenta nemmeno di analizzare una stringa di pianificazione cron complessa. Quindi, ecco la mia versione, usando croniter . Sintesi di 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()

Routine di aiuto:

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)

Come qualcuno potrebbe andare nella "mancata esecuzione" elif? Atm Sto usando un programma come l' "* * * * *"aggiunta di un po ' time.sleeppiù di 1 minuto in "Fai la tua cosa periodica" if, ma vedo sempre le cose in quell'istruzione if. Quando ci vuole più di 1 minuto vedo solo il ciclo while che salta l'esecuzione del ciclo mancante.
TPPZ,

@TPPZ Il processo avrebbe potuto essere sospeso, l'orologio avrebbe potuto essere modificato manualmente o da NTTP ecc. Ecc. Croniter è utilizzato in Airflow e sembra più completo rispetto al modulo Crontab e altri.
dlamblin,

7

Ho modificato la sceneggiatura.

  1. Facile da usare:

    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. Prova ad avviare l'attività nel primo secondo di un minuto.

Codice su Github


6

Ho una correzione minore per il metodo di esecuzione della classe CronTab suggerito da Brian .

Il tempo era scaduto di un secondo, portando a un secondo, loop continuo alla fine di ogni minuto.

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

Non esiste un modo "puro Python" per farlo perché alcuni altri processi dovrebbero avviare Python per eseguire la soluzione. Ogni piattaforma avrà uno o venti modi diversi per avviare i processi e monitorare i loro progressi. Su piattaforme unix, cron è il vecchio standard. Su Mac OS X c'è anche launchd, che combina il lancio cron-like con la funzionalità watchdog che può mantenere vivo il tuo processo se è quello che vuoi. Una volta che python è in esecuzione, è possibile utilizzare il modulo sched per pianificare le attività.


4

So che ci sono molte risposte, ma un'altra soluzione potrebbe essere quella di andare con i decoratori . Questo è un esempio per ripetere una funzione ogni giorno in un momento specifico. Il bello pensare di usare questo modo è che devi solo aggiungere lo zucchero sintattico alla funzione che vuoi programmare:

@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

E il decoratore apparirà come:

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 soluzione di Brian funziona abbastanza bene. Tuttavia, come altri hanno sottolineato, c'è un bug sottile nel codice di esecuzione. Inoltre l'ho trovato eccessivamente complicato per le esigenze.

Ecco la mia alternativa più semplice e funzionale per il codice di esecuzione nel caso in cui qualcuno ne abbia bisogno:

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

Un'altra soluzione banale sarebbe:

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)

E la classe aqcron.At è:

# 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
Fai attenzione quando pubblichi copia e incolla le risposte testuali / testuali a più domande, queste tendono ad essere contrassegnate come "spam" dalla community. Se lo stai facendo, di solito significa che le domande sono duplicate, quindi segnalale come tali: stackoverflow.com/a/12360556/419
Kev

1

Se stai cercando uno scheduler distribuito, puoi dare un'occhiata a https://github.com/sherinkurian/mani - ha bisogno di redis anche se quindi potrebbe non essere quello che stai cercando. (nota che sono l'autore) questo è stato creato per garantire la tolleranza agli errori facendo funzionare l'orologio su più di un nodo.


0

Non so se esiste già qualcosa del genere. Sarebbe facile scrivere il tuo con i moduli ora, datetime e / o calendario, vedi http://docs.python.org/library/time.html

L'unica preoccupazione per una soluzione Python è che il tuo lavoro deve essere sempre in esecuzione e possibilmente essere automaticamente "resuscitato" dopo un riavvio, qualcosa per il quale devi fare affidamento su soluzioni dipendenti dal sistema.


3
Roll your your è un'opzione, anche se il codice migliore è il codice che non devi scrivere. Resurrezione, suppongo sia qualcosa che potrei dover considerare.
James


0

Metodo di Crontab sul server.

Nome file Python hello.py

Step1: Crea un file sh lascia che dia il nome s.sh

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

Step2: Apri Crontab Editor

crontab -e

Passaggio 3: Aggiungi orario di pianificazione

Usa la formattazione Crontab

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

Questo cron eseguirà "Al minuto 2."


0

Mi piace come il pacchetto pycron risolva questo problema.

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
Questa non è una buona idea, perché il tuo codice "print ('running backup')" avvierà l'intero minuto con un intervallo di 5 secondi. Quindi in questo caso il ritardo dovrebbe essere di 60 secondi.
n158,

Hai ragione! Grazie per la segnalazione.
Duffau
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.