¿Cómo obtengo un programador Cron like en Python? [cerrado]

348

Estoy buscando una biblioteca en Python que proporcione aty me cronguste la funcionalidad.

Me gustaría tener una solución pura de Python, en lugar de confiar en las herramientas instaladas en la caja; De esta manera corro en máquinas sin cron.

Para aquellos que no están familiarizados con cron: puede programar tareas basadas en una expresión como:

 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 sintaxis de la expresión cron time es menos importante, pero me gustaría tener algo con este tipo de flexibilidad.

Si no hay algo que haga esto por mí de inmediato, cualquier sugerencia para que los bloques de construcción hagan algo como esto sería gratamente recibida.

Editar No estoy interesado en iniciar procesos, solo "trabajos" también escritos en Python - funciones de Python. Por necesidad, creo que este sería un hilo diferente, pero no en un proceso diferente.

Con este fin, estoy buscando la expresividad de la expresión de tiempo cron, pero en Python.

Cron ha existido durante años, pero estoy tratando de ser lo más portátil posible. No puedo confiar en su presencia.

jamesh
fuente
1
También me gustaría saber cómo hacer esto. Sería más útil tener una solución multiplataforma que depender de componentes específicos de la plataforma.
Sean
66
Esto no está fuera de tema, esta es una pregunta muy importante y útil
Connor

Respuestas:

571

Si está buscando algo programa de pago ligero :

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)

Divulgación : soy el autor de esa biblioteca.

dbader
fuente
77
Deberías mencionar que eres el mantenedor de schedule. Funciono bien para mi. Sería aún mejor si tuviera una sintaxis cron y decoradores compatibles (vea crython pero no use esta biblioteca porque no funciona; la programación no parece estar bien escrita).
Tim Ludwinski
23
¿Hay alguna manera de pasar un parámetro al trabajo? Me gustaría hacer algo como esto: schedule.every (). Hour.do (job (myParam))
Zen Skunkworx
55
schedule.every (). hour.do (trabajo) ¿esto se ejecuta cada hora del reloj? ¿Como 01:00, 02:00, 03:00, etc.? incluso si la hora de inicio no es una hora completa?
swateek
1
supongamos que este código está en Scheduler.py. ¿se ejecutará este código automáticamente?
Kishan
25
@ darrel-holt y @ zen-skunkworx: la do()función reenvía argumentos adicionales que le pasa a la función de trabajo: schedule.readthedocs.io/en/stable/api.html#schedule.Job.do Por ejemplo, puede hacer esto : schedule.every().hour.do(job, param1, param2)No es necesario usar una lambda. Espero que esto ayude :)
dbader
65

Simplemente podría usar la sintaxis de paso de argumento normal de Python para especificar su crontab. Por ejemplo, supongamos que definimos una clase de evento de la siguiente manera:

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: no probado exhaustivamente)

Entonces su CronTab se puede especificar en la sintaxis normal de Python como:

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

De esta manera, obtienes todo el poder de la mecánica de argumento de Python (mezclando argumentos posicionales y de palabras clave, y puedes usar nombres simbólicos para nombres de semanas y meses)

La clase CronTab se definiría simplemente como dormir en incrementos de minutos y llamar a check () en cada evento. (Probablemente hay algunas sutilezas con el horario de verano / zonas horarias a tener en cuenta). Aquí hay una implementación rápida:

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)

Algunas cosas a tener en cuenta: los días de semana / meses de Python están indexados a cero (a diferencia de cron), y ese rango excluye el último elemento, por lo tanto, una sintaxis como "1-5" se convierte en rango (0,5), es decir, [0,1,2, 3,4]. Sin embargo, si prefiere la sintaxis cron, el análisis no debería ser demasiado difícil.

Brian
fuente
Es posible que desee agregar algunas declaraciones de importación para los inexpertos. Terminé colocando todas las clases en un solo archivo con from datetime import * from time import sleep y cambié time.sleep a sleep. Agradable, elegante solución simple. Gracias.
mavnn
1
Me pregunto, ¿por qué se prefiere esto a Kronos? ¿Está programado ese buggy (ya que kronos usa sched)? ¿O es esto simplemente anticuado?
cregox
Gracias Brian, uso tu solución en producción y está funcionando bastante bien. Sin embargo, como otros han señalado, hay un error sutil en su código de ejecución. También me pareció demasiado complicado para las necesidades.
raph.amiard
1
Esto es genial, pero aún no es compatible con la notación de barra, para la ejecución cada hora, min, etc ...
Chris Koston
1
Excelente idea para escribir sus propias clases, por ejemplo, cuando no tengo acceso a sudo en un servidor y, por lo tanto, no puedo pip install anything:)
Cometsong
27

Una cosa que he visto en mis búsquedas es el schedmódulo de Python, que podría ser el tipo de cosa que estás buscando.

Sean
fuente
11
sched ahora tiene entebs () que realiza una programación absoluta.
Jerther
55
Espero que esta sea la respuesta preferida ya que sched es parte de python2 y 3 stdlib ahora y hace una programación absoluta.
Michael
11

Más o menos igual que el anterior pero concurrente 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()
Hackeron
fuente
Solo una nota de que datetime.timetuple () comenzará con año, mes, día ... etc ...
Trey Stout
9

Ninguna de las soluciones enumeradas intenta siquiera analizar una cadena de programación cron compleja. Entonces, aquí está mi versión, usando croniter . Esencia básica:

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

Rutinas de ayuda:

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)
rublo
fuente
¿Cómo alguien podría entrar en la "ejecución perdida" elif? Atm Estoy usando un horario como "* * * * *"luego agregar algo time.sleepmás de 1 minuto en "Haz tu cosa periódica" if, pero siempre veo las cosas en esa declaración if. Cuando se tarda más de 1 minuto, solo veo el ciclo while omitiendo la ejecución del ciclo faltante.
TPPZ
@TPPZ El proceso podría haberse suspendido, el reloj podría haberse cambiado manualmente o por ntp, etc. etc. Croniter se usa en Airflow y parece tener más funciones que el módulo Crontab y otros.
dlamblin
7

He modificado el guión.

  1. Fácil de usar:

    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. Intenta comenzar la tarea en el primer segundo de un minuto.

Código en Github

ning
fuente
6

Tengo una solución menor para el método de ejecución de clase CronTab sugerido por Brian .

El tiempo de espera se agotó en un segundo, lo que condujo a un bucle duro de un segundo al final de cada 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()
benc
fuente
4

No hay una forma de "pitón puro" para hacer esto porque algún otro proceso tendría que iniciar python para ejecutar su solución. Cada plataforma tendrá una o veinte formas diferentes de iniciar procesos y monitorear su progreso. En plataformas Unix, cron es el viejo estándar. En Mac OS X también hay launchd, que combina el lanzamiento cron-like con la funcionalidad de watchdog que puede mantener vivo su proceso si eso es lo que desea. Una vez que Python se está ejecutando, puede usar el módulo programado para programar tareas.

Mella
fuente
4

Sé que hay muchas respuestas, pero otra solución podría ser ir con decoradores . Este es un ejemplo para repetir una función todos los días a una hora específica. Lo mejor de usar esta manera es que solo necesita agregar el azúcar sintáctico a la función que desea programar:

@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

Y el decorador se verá así:

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
Damia Fuentes
fuente
1

La solución de Brian está funcionando bastante bien. Sin embargo, como otros han señalado, hay un error sutil en el código de ejecución. También me pareció demasiado complicado para las necesidades.

Aquí está mi alternativa más simple y funcional para el código de ejecución en caso de que alguien lo necesite:

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)
raph.amiard
fuente
1

Otra solución trivial sería:

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)

Y la clase aqcron. Es:

# 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
fdb
fuente
1
Tenga cuidado al publicar copiar y pegar respuestas repetitivas / textuales a múltiples preguntas, la comunidad suele marcarlas como "spam". Si está haciendo esto, generalmente significa que las preguntas son duplicadas, así que márquelas como tales en su lugar: stackoverflow.com/a/12360556/419
Kev
1

Si está buscando un programador distribuido, puede consultar https://github.com/sherinkurian/mani ; sin embargo, necesita redis, por lo que podría no ser lo que está buscando. (tenga en cuenta que yo soy el autor) esto se creó para garantizar la tolerancia a fallos al ejecutar el reloj en más de un nodo.

shrnkrn
fuente
0

No sé si ya existe algo así. Sería fácil escribir uno propio con módulos de hora, fecha y hora o calendario, consulte http://docs.python.org/library/time.html

La única preocupación de una solución pitón es que sus necesidades de trabajo para estar siempre corriente y eventualmente ser automáticamente "resucitados" después de un reinicio, algo para lo que hace necesario recurrir a soluciones que dependen del sistema.

Davide
fuente
3
Roll your own es una opción, aunque el mejor código es el código que no tiene que escribir. Resurrección, supongo que es algo que debo considerar.
jamesh
0

Método de Crontab en el servidor.

Nombre de archivo Python hello.py

Paso 1: crea un archivo sh, dé el nombre s.sh

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

Paso 2: Abra el Editor de Crontab

crontab -e

Paso 3: Agregar tiempo de programación

Usar formato de tabla de referencias

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

Este cron se ejecutará "en el minuto 2".

shaurya uppal
fuente
0

Me gusta cómo el paquete pycron resuelve este 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)
Duffau
fuente
1
Esta no es una buena idea, porque su código "print ('ejecutando copia de seguridad')" se lanzará minutos completos con un intervalo de 5 segundos. Entonces, en este caso, el retraso debe ser de 60 segundos.
n158
¡Tienes razón! Gracias por señalar eso.
Duffau