Планировщик задач: напоминалки
Учим Цыплёнка делать то, чего раньше он не умел совсем: сам, без единого сообщения от пользователя, написать тебе в нужный момент — «не забудь про домашку!».
Планировщик задач — это «будильник» внутри бота: ты заранее говоришь, что и когда нужно сделать, а он сам в назначенное время вызывает твою функцию — даже если в этот момент тебе никто ничего не пишет.
Зачем боту вообще будильник
Смотри, какая штука. Всё, что мы делали с Цыплёнком до сих пор, работало по одной схеме: пользователь пишет — бот отвечает. Команда /start пришла — хэндлер сработал. Кнопку нажали — callback прилетел. Бот всё время реагирует. Он как продавец в магазине: пока к нему не подошли, он просто стоит за прилавком.
А теперь представь бот-напоминалку про домашку. Ты вечером говоришь ему: «напомни мне про реферат завтра в 18:00». И всё, закрываешь Telegram, идёшь гулять, играть, спать. Завтра в 18:00 тебе никто не пишет — но бот сам, по своей инициативе, присылает: «Эй, ты хотел сесть за реферат 🐥». Вот это «сам, без входящего сообщения» — принципиально новая способность. Реагировать на сообщения бот умел, а вот действовать по времени — ещё нет.
Вот к чему мы придём в этом уроке:
Пользователь: /напомни 10 покормить кота
Бот: Окей! Через 10 секунд напомню 🐥
(проходит 10 секунд, пользователь ничего не пишет)
Бот: ⏰ Напоминание: покормить котаРезультат: в чате пользователь один раз попросил напомнить, а спустя заданное время бот сам прислал напоминание — без всякого нового сообщения от человека. Между этими двумя строками пользователь вообще не трогал телефон.
Чтобы такое сделать, нам нужен механизм, который «держит в голове» список будущих дел и сам срабатывает в нужную секунду. Этот механизм называется планировщик задач, и для aiogram удобнее всего взять готовую библиотеку — APScheduler.
Что такое планировщик: метафора будильника
Самая близкая аналогия — будильник в телефоне. Ты не сидишь всю ночь и не смотришь на часы, чтобы проснуться в 7:00. Ты один раз ставишь будильник и спокойно засыпаешь — а телефон сам следит за временем и звенит, когда нужно. Планировщик — это ровно такой будильник, только вместо «зазвенеть» он вызывает твою функцию Python.
Поставить будильник = добавить задачу в планировщик. «Зазвенел» = планировщик сам вызвал твою функцию. Что делать при звонке — пишешь ты сам внутри этой функции.
Библиотека, которая даёт нам такой будильник, называется APScheduler (читается «эй-пи-скедьюлер», расшифровывается как Advanced Python Scheduler — «продвинутый планировщик для Python»). Она умеет три типа «звонков»:
- date — позвонить один раз в конкретный момент. «Напомни в 18:00 сегодня» или «через 10 секунд». Сработал один раз — и забыл.
- interval — звонить каждые столько-то времени. «Каждые 30 минут проверяй новые задания». Повторяется бесконечно, пока не выключишь.
- cron — звонить по расписанию, как в календаре. «Каждый будний день в 8:00» или «каждый понедельник». Это как умный будильник, который знает про дни недели.
Тебе не нужно вручную следить за временем, заводить бесконечные циклы с sleep или как-то ещё извращаться. Ты просто говоришь планировщику: «вот функция, вызови её тогда-то» — а он берёт всю возню со временем на себя.
Почему не просто sleep?
У новичка возникает соблазн: «а давай я просто напишу await asyncio.sleep(600) и потом отправлю сообщение?» Для одного напоминания это даже сработает. Но представь, что напоминалкой пользуются десять друзей и у каждого по пять напоминаний. Получится куча «спящих» кусков кода, за которыми невозможно уследить: ты не можешь их посмотреть списком, не можешь отменить одно конкретное, и если бот перезапустится — все они просто исчезнут. Планировщик же держит все задачи в одном месте, как список будильников в телефоне: их видно, ими можно управлять, их можно удалять по одному.
Подключаем APScheduler к боту
Сначала ставим библиотеку. В терминале, там же, где у тебя установлен aiogram:
pip install apschedulerДальше работаем в нашем bot.py — том самом файле, где уже живут объекты bot и dp. Для асинхронного бота берётся специальный планировщик AsyncIOScheduler: он умеет работать в той же «асинхронной кухне», что и aiogram, и не блокирует бота, пока ждёт нужного времени.
from apscheduler.schedulers.asyncio import AsyncIOScheduler
# рядом с объявлением bot и dp
scheduler = AsyncIOScheduler(timezone="Europe/Moscow")Результат: в чате это ничего не показывает — мы лишь создали «будильник», но ещё не завели его. Обрати внимание на timezone: планировщик должен знать твой часовой пояс, иначе он будет считать время по Гринвичу, и «напомни в 18:00» сработает на несколько часов мимо. Указывай свой пояс явно — для Москвы это Europe/Moscow.
Создать планировщик мало — его нужно ещё и запустить, иначе он не будет следить за временем. Запускают его прямо перед стартом бота, в том же месте, где мы вызываем polling.
import asyncio
async def main():
scheduler.start() # заводим будильник
await dp.start_polling(bot) # запускаем приём сообщений
if __name__ == "__main__":
asyncio.run(main())Результат: в чате опять ничего не видно, но теперь планировщик «проснулся» и готов принимать задачи. Важно: scheduler.start() зовём до start_polling и в той же асинхронной функции — тогда будильник и бот крутятся на одной кухне и не мешают друг другу.
Пример 1. Отложенное напоминание (тип date)
Начнём с самого понятного — напомнить один раз через сколько-то секунд. Нам понадобятся две вещи: функция, которую планировщик вызовет в нужный момент, и сам момент добавления задачи в хэндлере.
Сначала функция, которая отправляет напоминание. Заметь: она async и сама вызывает bot.send_message — ведь отправлять-то будет бот, причём в тот момент, когда пользователь молчит. Значит, нам нужно заранее знать, кому писать. Кому писать — это chat id, который мы возьмём из сообщения, когда пользователь нас попросит.
async def send_reminder(chat_id: int, text: str):
await bot.send_message(chat_id, f"⏰ Напоминание: {text}")Результат: сама по себе эта функция ничего не делает — это «что сказать, когда зазвонит будильник». Дальше мы попросим планировщик вызвать её в нужное время.
Теперь хэндлер команды /напомни. Формат придумаем простой: /напомни 10 покормить кота — где 10 это через сколько секунд, а дальше текст напоминания.
from datetime import datetime, timedelta
from aiogram.filters import Command
from aiogram.types import Message
@dp.message(Command("напомни"))
async def cmd_remind(message: Message):
parts = message.text.split(maxsplit=2)
if len(parts) < 3:
await message.answer("Формат: /напомни 10 текст напоминания")
return
seconds = int(parts[1])
text = parts[2]
run_at = datetime.now() + timedelta(seconds=seconds)
scheduler.add_job(
send_reminder,
trigger="date",
run_date=run_at,
args=[message.chat.id, text],
)
await message.answer(f"Окей! Через {seconds} секунд напомню 🐥")Результат: в чате на сообщение /напомни 10 покормить кота бот сразу ответит «Окей! Через 10 секунд напомню 🐥», а через 10 секунд сам пришлёт «⏰ Напоминание: покормить кота». Если команду написать без текста, бот вежливо подскажет формат.
Разберём по шагам, что здесь происходит:
message.text.split(maxsplit=2)режет строку максимум на три куска: сама команда, число секунд и весь остальной текст одним куском. Безmaxsplit=2длинное напоминание развалилось бы по пробелам.datetime.now() + timedelta(seconds=seconds)— это «сейчас плюс столько-то секунд», то есть тот самый момент, когда должен зазвонить будильник.timedelta— это «промежуток времени», его удобно прибавлять к текущему моменту.scheduler.add_job(...)— главная строка. Мы говорим планировщику: «вызови функциюsend_reminder, тип звонка —date(один раз), момент —run_at, а вот аргументы, которые надо передать в функцию».args=[message.chat.id, text]— именно так мы заранее «кладём в конверт» того, кому и что написать. Когда через 10 секунд будильник зазвонит, он вызоветsend_reminder(chat_id, text)с этими значениями.
Главное, что нужно прочувствовать: хэндлер срабатывает сразу и просто «ставит будильник», а send_reminder вызывается потом, отдельно, когда пользователь уже ничего не пишет. Это два разных момента времени.
Маленький чистый пример: считаем будущее время
Чтобы поиграть с timedelta без всякого бота, вот самодостаточный сниппет на стандартной библиотеке — его можно запустить прямо здесь.
from datetime import datetime, timedelta
now = datetime(2026, 6, 19, 17, 50, 0) # как будто сейчас 17:50
run_at = now + timedelta(seconds=600) # через 600 секунд = 10 минут
print("сейчас:", now.strftime("%H:%M"))
print("сработает в:", run_at.strftime("%H:%M"))Вывод:
сейчас: 17:50 сработает в: 18:00
Видишь, как timedelta(seconds=600) сдвинул время ровно на 10 минут вперёд? Ровно это число планировщик и берёт как момент «звонка». А strftime("%H:%M") — это способ показать время по-человечески, в формате «часы:минуты».
Пример 2. Повторяющееся напоминание (тип interval)
Теперь сделаем то, чего одним sleep уже не сделаешь красиво: напоминание, которое повторяется. Например, бот для тренировок, который каждые 30 минут пишет «встань, разомнись, попей воды». Для повторений берём тип interval.
@dp.message(Command("разминка"))
async def cmd_workout(message: Message):
scheduler.add_job(
send_reminder,
trigger="interval",
minutes=30,
args=[message.chat.id, "встань и разомнись 🤸"],
id=f"workout_{message.chat.id}",
replace_existing=True,
)
await message.answer("Буду напоминать про разминку каждые 30 минут 🐥")Результат: в чате на /разминка бот ответит, что будет напоминать, и дальше каждые 30 минут сам присылает «⏰ Напоминание: встань и разомнись 🤸» — снова и снова, пока задачу не отменишь.
Здесь два новых важных параметра, на которые стоит обратить внимание:
id=f"workout_{message.chat.id}"— мы даём задаче имя. Это как подписать будильник, чтобы потом найти именно его среди других. Имя делаем уникальным для каждого пользователя, подмешивая его chat id.replace_existing=True— «если такая задача с таким именем уже есть, замени её, а не создавай вторую». Без этого, если человек дважды напишет/разминка, у него заведётся два одинаковых будильника, и напоминания будут приходить по два раза. С этим флагом второй вызов просто перезапишет первый.
А раз у задачи есть имя, её можно и отменить. Сделаем команду «хватит»:
@dp.message(Command("хватит"))
async def cmd_stop(message: Message):
job_id = f"workout_{message.chat.id}"
job = scheduler.get_job(job_id)
if job is None:
await message.answer("А я и так ничего не напоминаю 🙂")
return
scheduler.remove_job(job_id)
await message.answer("Окей, больше не буду напоминать про разминку 🐥")Результат: в чате на /хватит бот находит будильник по его имени и удаляет его; если такого будильника не было — мягко сообщает, что напоминать и нечего. Проверка job is None здесь обязательна: если попытаться удалить несуществующую задачу напрямую, APScheduler поднимет ошибку.
Пример 3. Расписание по часам (тип cron)
Последний тип — cron, расписание как в календаре. Допустим, мы хотим, чтобы Цыплёнок каждый будний день в 8 утра писал тебе «не забудь дневник и физру 🐥». Тут не подходит ни «один раз», ни «каждые N минут» — нужно именно «по будням в 8:00».
scheduler.add_job(
send_reminder,
trigger="cron",
day_of_week="mon-fri",
hour=8,
minute=0,
args=[my_chat_id, "собери портфель: дневник и физра 🐥"],
id="school_morning",
replace_existing=True,
)Результат: в чате каждый будний день ровно в 8:00 бот сам пришлёт «⏰ Напоминание: собери портфель: дневник и физра 🐥», а в выходные будет молчать. Здесь day_of_week="mon-fri" значит «с понедельника по пятницу», а hour=8, minute=0 — «в 8:00». Именно ради такого расписания и нужен часовой пояс, который мы задали при создании планировщика: без него «8 утра» оказалось бы вовсе не твоим утром.
Заметь, что здесь стоит my_chat_id — какое-то конкретное число. На практике этот id ты берёшь из базы (помнишь урок про SQLite для бота?), куда сохранил пользователей при первом /start. Расписание «по будням в 8:00» обычно ставят не в ответ на команду, а один раз при запуске бота — пройдясь по всем сохранённым пользователям.
Частые ошибки и подводные камни
Вот грабли, на которые наступает почти каждый, кто впервые прикручивает планировщик. Прочитай заранее — сэкономишь себе вечер.
- Забыть вызвать scheduler.start(). Самая частая боль: код вроде написан, задачи добавляются, а напоминания не приходят. Причина — будильник создали, но не завели. Без
scheduler.start()вmain()планировщик просто спит и за временем не следит. - Передать в add_job вызов функции, а не саму функцию. Нужно писать
add_job(send_reminder, ...)— без скобок. Если написатьadd_job(send_reminder(...))со скобками, Python вызовет функцию прямо сейчас, а в планировщик попадёт её результат (скорее всегоNone). Передавай имя функции, а данные для неё клади отдельно вargs. - Не указать timezone. Если не задать часовой пояс, планировщик считает время по UTC (по Гринвичу), и расписание
cronсработает со сдвигом на несколько часов. Для отложенныхdate-задач это особенно коварно: всё «почти работает», просто не в то время. Всегда указывайtimezoneпри создании планировщика. - Создавать дубли задач без id и replace_existing. Если пользователь дважды нажмёт «начать напоминания», а ты не дал задаче имя и не поставил
replace_existing=True, заведётся два одинаковых будильника — и человек начнёт получать по два сообщения. Давай повторяющимся задачам уникальныйid. - Думать, что задачи переживут перезапуск бота. По умолчанию APScheduler держит задачи в оперативной памяти. Перезапустил бота — все запланированные напоминания исчезли, как будильники, сброшенные при выключении телефона. Если напоминания должны жить и после перезапуска, их нужно сохранять в базу (например, в ту же SQLite) и заново ставить при старте бота.
Мини-практика: напоминалка про домашку
Собери из кусочков урока свою команду /домашка, которая раз в день вечером спрашивает, сделал ли ты уроки. Это уже почти настоящий полезный бот.
- Сделай команду
/домашка ЧЧ:ММ(например/домашка 19:30), которая ставит cron-задачу на это время каждый день. Разбери введённое время черезparts[1].split(":")и преврати в числаhourиminute. - В функции напоминания пусть бот пишет что-то вроде «Ты сделал домашку на завтра? 📚 Если да — красавчик, если нет — самое время 🐥».
- Дай задаче уникальный
idвидаf"homework_{chat_id}"и поставьreplace_existing=True, чтобы повторный вызов команды переносил время, а не плодил дубли. - Добавь команду
/домашка_стоп, которая по тому жеidудаляет задачу — не забудь проверить, что задача вообще существует, черезget_job. - Проверь: поставь напоминание на минуту вперёд от текущего времени и убедись, что бот сам написал тебе в назначенную минуту, пока ты ничего ему не отправлял.
Подсказка: cron-задача «каждый день в ЧЧ:ММ» — это trigger="cron" с параметрами hour и minute, без day_of_week (тогда сработает во все дни недели).
Итоги и что дальше
Сегодня Цыплёнок-помощник научился действовать сам, по времени, а не только отвечать на сообщения. Мы подключили APScheduler — «будильник» внутри бота, создали AsyncIOScheduler с правильным часовым поясом и не забыли завести его через scheduler.start(). Разобрали три типа задач: date (один раз в нужный момент), interval (каждые N минут) и cron (по расписанию, как в календаре). Научились заранее «класть в конверт» chat id и текст через args, давать задачам уникальный id, заменять и отменять их, а ещё прошли по самым липким граблям — от забытого start() до неуказанного часового пояса.
Важно помнить вот что: чтобы рассылать напоминания, бот должен знать chat id своих пользователей — а это значит, что без сохранения людей в базу далеко не уедешь. Хорошо, что в диалогах вроде «во сколько напомнить?» ты уже умеешь вести пользователя по шагам — мы разбирали это в уроке про FSM и машину состояний. В следующем уроке мы соберём эти кусочки вместе и научим Цыплёнка делать массовую рассылку — отправлять сообщение сразу всем пользователям, аккуратно, не упираясь в лимиты Telegram.