четверг, 1 декабря 2011 г.

Парсим выражение crontab

Crontab - формат файла заданий для планировщика cron. Знаменит простотой, гибкостью и мощью, позволяющими задать практически любую периодичность для выполнения заданий.

Как только выяснилось, что придется писать свой планировщик, мой взгляд сразу обратился на crontab. Но, испугавшись кажущейся сложности его парсинга, я начал придумывать другой, возможно более слабый, но простой в реализации, механизм периодичности задач. В этом неблагодарном деле я не преуспел и, несолоно хлебавши, вернулся к кронтабу. Как оказалось, бояться там было абсолютно нечего.

Формат я сразу несколько упростил для облегчения работы себе любимому:
  • не разрешено использовать символьные обозначения месяцев и дней недели (jan - dec, mon - sun), только номера; 
  • для обозначения воскресенья используется только 7, 0 не используется.

Задачей парсинга крон-выражения является получение из исходной пятисекционной строки кортежа из пяти множеств, содержащих все допустимые номера для каждой секции. Этакая "компиляция" строки в набор чисел. :)

Формально синтаксис секции крона представляет собой список диапазонов с шагом, с несколькими сокращениями и умолчаниями. Поняв это, легко пишем несложную функцию парсинга:

__bounds = {
    0: (0, 59),
    1: (0, 23),
    2: (1, 31),
    3: (1, 12),
    4: (1, 7)
}

def parse_cron(self, cron):
    u"""Парсить строку cron"""
    
    parts = re.split(ur'\s+', cron)
    if len(parts) != 5:
        raise ValueError(u"Некорректный формат cron, строка '{0}' должна содержать 5 элементов через пробелы.")
    
    compiled = []
    for i, part in enumerate(parts):
        partcompiled = set()
        for item in part.split(u','):
            if item:
                if u'/' in item:
                    val, step = item.split(u'/')
                    step = int(step)
                else:
                    val, step = item, 1
                if u'*' == val:
                    start, stop = self.__bounds[i]
                elif u'-' in val:
                    start, stop = [int(b) for b in val.split(u'-')]
                else:
                    start, stop = int(val), int(val)
                stop += 1
                
                partcompiled |= set(range(start, stop, step))
        compiled.append(partcompiled)
    return tuple(compiled)

Использование полученного кортежа тривиально: проверяемую дату/время разбить на составляющие и проверить вхождение каждой составляющей в соответствующее множество. Для получения номера дня недели следует использовать функцию datetime.isoweekday, ее возвращаемое значение соответствует принятому соглашению о нумерации дней недели.

Я поленился, но можно избавиться от введенных ограничений:
  1. перед парсингом привести исходную строку к нижнему регистру и заменить в ней символьные название месяцев и дней недели на соответствующие номера;
  2. после парсинга в последнем множестве (дни недели) заменить 0 на 7, если присутствует.
 После этого получится парсер классического крон-формата.

У кронтаба есть ограничения:
  1. Точность срабатывания планировщика не выше минуты. Для подавляющего большинства задач это абсолютно некритично, но допускаю существование ситуаций, где это имеет большое значение;
  2. Нельзя задать период срабатывания планировщика, некратный длительности секции, например выполнять задание каждые 100 минут (длительность секции - 60 мин.). Для задач, где необходимо уметь задавать любой постоянный промежуток между запуском заданий, крон тоже не подойдет.
Из первого ограничения следует, что для нормальной работы программы проверка времени срабатывания должна происходить не реже раза в минуту. Чем чаще будет проверяться время срабатывания, тем ближе к началу минуты срабатывание будет происходить. Я обычно проверяю время каждые 15 секунд.

Комментариев нет:

Отправить комментарий