урок 2 из 10

Первый бот: Bot, Dispatcher и async

Готовить проект в своём виртуальном окружении; понимать роли Bot, Dispatcher и обработчика в минимальном боте; писать асинхронные обработчики с await и не блокировать бота обычным ожиданием; знать, что обработчики проверяются сверху вниз, проверять наличие текста и перезапускать бота после правки.

10 мин чтения · 4 главы ·4 задания ·домашняя работа

Окружение проекта

Прежде чем писать код, готовят место, где он будет жить. Бот — это проект, и у проекта должна быть своя папка и своё виртуальное окружение: отдельный набор библиотек, который не смешивается с другими проектами на компьютере. Без него через полгода окажется, что для одного проекта нужна одна версия библиотеки, для другого — другая, и оба ломаются.

mkdir mybot
cd mybot
python -m venv venv          # создать окружение в папке venv
venv\Scripts\activate        # Windows
source venv/bin/activate     # macOS и Linux
pip install aiogram          # поставить библиотеку

После активации в начале строки терминала появляется (venv) — знак того, что команды python и pip работают внутри окружения. Если его нет, библиотека встанет не туда, и программа потом скажет, что aiogram не найден. Это самая частая ошибка первого вечера.

Версия Python нужна свежая — 3.10 или новее: библиотека использует новые возможности языка. Узнать версию можно командой python --version. Если на компьютере несколько версий, окружение создают той, что новее.

Код бота пишут в любом редакторе, но удобнее тот, что подсказывает имена и подсвечивает ошибки: он заранее покажет, что вы забыли импорт или ошиблись в названии. Главный файл обычно называют bot.py или main.py. Папку venv в общий репозиторий не кладут — её можно пересоздать одной командой, а весит она много.

Двадцать строк бота

Вот бот, который отвечает на команду /start. Это вся программа — двадцать строк, и в каждой есть смысл.

import asyncio
import logging

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message

TOKEN = "сюда токен"   # в седьмом уроке уберём его из кода

bot = Bot(token=TOKEN)
dp = Dispatcher()

@dp.message(CommandStart())
async def start(message: Message):
    await message.answer("Привет! Я бот.")

async def main():
    await dp.start_polling(bot)

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(main())
Botговорит с Bot APIзнает токенDispatcherполучает обновлениявыбирает обработчикОбработчикasync-функцияпишет ответstart_polling(bot) запускает круг
Bot умеет говорить с Телеграмом, Dispatcher решает, какой обработчик вызвать, обработчик — ваша функция с ответом. Запуск опроса связывает их вместе.

Bot — объект, который умеет говорить с Телеграмом: он знает токен и вызывает методы Bot API. Dispatcher — распорядитель: он получает обновления и решает, какую из ваших функций вызвать. Функция, помеченная @dp.message(...), — это обработчик: «когда придёт сообщение, подходящее под условие, вызови меня». Условие здесь — CommandStart(), то есть команда /start.

Обработчик получает message — пришедшее сообщение со всеми его полями — и отвечает методом message.answer. Ответ уходит в тот же чат, откуда пришло сообщение: номер чата библиотека берёт сама.

Функция main запускает опрос: dp.start_polling(bot) бесконечно спрашивает Телеграм о новых обновлениях и передаёт их распорядителю. Строка asyncio.run(main()) запускает всё это, а logging.basicConfig включает журнал: в терминале будет видно, что бот запустился и какие обновления обрабатывает. Без журнала бот работает молча, и понять, почему он не отвечает, гораздо труднее.

async и await

Слова async и await пугают новичков больше всего, хотя идея за ними простая. Бот общается с сетью: отправить ответ — значит послать запрос в Телеграм и дождаться, пока он дойдёт. Это ожидание длится десятки и сотни миллисекунд. Если в это время программа просто стоит, остальные люди ждут своей очереди.

time.sleep(5) — блокируетАня ждёт 5 сБорис ждёт 10 сВера ждёт 15 сawait asyncio.sleep(5)Аня ждёт 5 сБорис ждёт 5 сВера ждёт 5 сawait — «пока жду, займись другими»
Пока один ответ ждёт сеть, программа успевает заняться другими людьми. Обычное ожидание остановило бы всех; await отдаёт очередь соседям.

async def объявляет асинхронную функцию — такую, которая умеет уступать очередь. await ставят перед каждым действием, где нужно подождать: отправить сообщение, скачать файл, обратиться к базе через асинхронную библиотеку. На время ожидания программа переключается на другие обновления, а потом возвращается и продолжает. Поэтому один небольшой бот легко обслуживает сотни человек одновременно.

Отсюда две ошибки, которые встречаются у всех.

Первая — забытый await. Строка message.answer("Привет") без await ничего не отправит: функция вернёт «обещание» выполнить работу, но выполнять его никто не станет. Бот молчит, а в журнале появляется предупреждение, что корутина так и не была дождана. Правило: каждый метод бота и сообщения, который общается с Телеграмом, вызывается через await.

Вторая — блокирующее ожидание. Обычный time.sleep(5) внутри обработчика останавливает не одного человека, а всю программу: пять секунд не отвечает никто. Если нужно подождать, пишут await asyncio.sleep(5) — ждёт только этот обработчик, остальные работают. То же с любой долгой обычной операцией: тяжёлый расчёт или медленный запрос к сайту обычной библиотекой замораживают бота целиком.

Эхо и запуск

Добавим второй обработчик — эхо: бот повторяет всё, что ему написали. На нём хорошо видно, как распорядитель выбирает обработчик.

@dp.message()
async def echo(message: Message):
    if message.text:
        await message.answer(message.text)
    else:
        await message.answer("Я пока понимаю только текст.")

@dp.message() без условия ловит любое сообщение. Распорядитель проверяет обработчики по порядку, сверху вниз, и вызывает первый подходящий. Поэтому обработчик /start стоит выше эха: иначе эхо перехватило бы и команду, и бот ответил бы на /start словом «/start». Общий обработчик ставят последним.

Обратите внимание на проверку if message.text. Человек может прислать фото, стикер или голосовое — у такого сообщения текста нет, поле пустое. Попытка отправить пустой текст закончилась бы ошибкой. Бот, который падает от стикера, — классика первого вечера.

Запускают бота из терминала командой python bot.py (с активным окружением). В журнале появится строка о начале опроса, и бот начнёт отвечать. Остановить — Ctrl+C. После каждой правки кода бота перезапускают: работающая программа не видит изменений в файле.

Если бот не отвечает, проверяют по порядку: запущена ли программа и нет ли в терминале ошибки; тот ли токен; не запущена ли вторая копия; написали ли вы именно этому боту. Почти всегда причина в одном из четырёх.

Задания урока

Первые задание решается прямо здесь — без регистрации. Щёлкните по ответу.

  1. 1. Где поставить обработчик без условия @dp.message()?

Остальные задания урока — в курсе, с сохранением прогресса и разбором ошибок.

  1. Если в начале строки терминала нет «(venv)», pip поставит библиотеку в окружение проекта.легко
  2. Сопоставьте деталь и её роль.средне
  3. Строка message.answer("Привет") без await отправит ответ, просто чуть позже.средне
Пройти урок с проверкой заданий Курс «Телеграм-боты на Python» открыт и бесплатен, прогресс сохраняется.
Открыть курс