Окружение проекта
Прежде чем писать код, готовят место, где он будет жить. Бот — это проект, и у проекта должна быть своя папка и своё виртуальное окружение: отдельный набор библиотек, который не смешивается с другими проектами на компьютере. Без него через полгода окажется, что для одного проекта нужна одна версия библиотеки, для другого — другая, и оба ломаются.
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 — распорядитель: он получает обновления и решает, какую из ваших функций вызвать. Функция, помеченная @dp.message(...), — это обработчик: «когда придёт сообщение, подходящее под условие, вызови меня». Условие здесь — CommandStart(), то есть команда /start.
Обработчик получает message — пришедшее сообщение со всеми его полями — и отвечает методом message.answer. Ответ уходит в тот же чат, откуда пришло сообщение: номер чата библиотека берёт сама.
Функция main запускает опрос: dp.start_polling(bot) бесконечно спрашивает Телеграм о новых обновлениях и передаёт их распорядителю. Строка asyncio.run(main()) запускает всё это, а logging.basicConfig включает журнал: в терминале будет видно, что бот запустился и какие обновления обрабатывает. Без журнала бот работает молча, и понять, почему он не отвечает, гораздо труднее.
async и await
Слова async и 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. После каждой правки кода бота перезапускают: работающая программа не видит изменений в файле.
Если бот не отвечает, проверяют по порядку: запущена ли программа и нет ли в терминале ошибки; тот ли токен; не запущена ли вторая копия; написали ли вы именно этому боту. Почти всегда причина в одном из четырёх.