1. Введение: Проблема масштабирования
В первой части нашего руководства мы успешно создали и запустили простого эхо-бота. Он работает, но его код, полностью размещенный в одном файле main.py
, обладает серьезным архитектурным недостатком, который проявится при первом же усложнении проекта.
Представьте, что нам нужно добавить 10, 20 или 50 новых команд и функций. Файл main.py
неизбежно превратится в неуправляемый монолит, где логика обработки команд смешается с административными функциями, а пользовательские сценарии будут разбросаны между десятками хэндлеров. Поддерживать и расширять такой код становится крайне сложно.
Для решения этой проблемы в aiogram 3 существует мощный и элегантный механизм — Роутеры (Routers). Они позволяют группировать обработчики (хэндлеры) по функциональному признаку и выносить их в отдельные модули, создавая чистую и масштабируемую структуру проекта.
В этой статье мы проведем рефакторинг нашего проекта, внедрим роутеры для организации кода и, используя новую структуру, научим бота распознавать разные типы контента: текст, фото и стикеры.
2. Теоретическая основа: Что такое Роутеры?
В aiogram Роутер (Router
) — это основной компонент, предназначенный для структурирования и организации кода обработки событий (апдейтов). Его главная задача — позволить разработчику группировать логически связанные обработчики (хэндлеры) вместе, изолируя их от другого функционала.
Представьте Dispatcher
как корневой узел или главный маршрутизатор вашего приложения. Он является точкой входа для всех обновлений, поступающих от Telegram API. Каждый экземпляр Router
, в свою очередь, — это отдельная ветвь в этой структуре, отвечающая за конкретный блок логики. Например, один роутер может содержать хэндлеры для базовых команд (/start
, /help
), второй — для административных функций, а третий — для обработки сценариев заказа товара.
С помощью метода dp.include_router(router)
вы, по сути, "подключаете" эту ветвь к главному диспетчеру. Когда от Telegram приходит новое событие (сообщение, нажатие кнопки), Dispatcher
последовательно передает его каждому подключенному роутеру. Роутер, в свою очередь, пытается найти внутри себя обработчик, фильтры которого соответствуют этому событию.
Такой подход обеспечивает ключевые преимущества для архитектуры приложения:
Модульность: Каждый блок функционала (например, команды администратора) находится в своем собственном файле, не затрагивая другие части кода.
Масштабируемость: Добавление новой функциональности сводится к созданию нового файла с роутером и его хэндлерами, с последующим подключением этого роутера в одной строке в главном файле.
Читаемость и поддержка:
main.py
остается чистым и выполняет только инициализацию. Легко найти нужный код, так как он структурирован по файлам, отвечающим за конкретные задачи.
3. Шаг 1: Первый рефакторинг. Внедряем Роутер в main.py
Прежде чем выносить логику в отдельные файлы, давайте проведем первый, самый простой этап рефакторинга. Мы внедрим Router
непосредственно в наш существующий файл main.py
. Это позволит нам наглядно увидеть, как роутер механически встраивается в приложение, не меняя при этом файловой структуры.
Для этого потребуется выполнить четыре простых действия:
Импортируем
Router
: В первую очередь, импортируем соответствующий класс из библиотекиaiogram
.Создаем экземпляр роутера: Сразу после инициализации диспетчера создаем экземпляр нашего роутера.
Переносим хэндлеры на роутер: Все декораторы обработчиков, которые раньше выглядели как
@dp.message(...)
, мы заменяем на@router.message(...)
. Таким образом, мы сообщаем, что эти хэндлеры теперь принадлежат не диспетчеру напрямую, а нашему новому роутеру.Регистрируем роутер в диспетчере: В главной асинхронной функции
main
нам нужно «сообщить» главному диспетчеру о существовании нашего роутера. Делается это с помощью методаinclude_router
.
Итоговый код в main.py
будет выглядеть следующим образом:
# main.py
import asyncio
import logging
# 1. Импортируем Router
from aiogram import Bot, Dispatcher, types, Router
from aiogram.filters.command import Command
logging.basicConfig(level=logging.INFO)
# Убедитесь, что здесь ваш токен
bot = Bot(token="ВАШ_ТОКЕН_ЗДЕСЬ")
dp = Dispatcher()
# 2. Создаем экземпляр роутера
router = Router()
# 3. "Вешаем" хэндлеры на роутер, а не на диспетчер
@router.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Привет! Этот код теперь работает с роутером.")
@router.message()
async def echo_message(message: types.Message):
await message.answer(f"Я получил твое сообщение: {message.text}")
async def main():
# 4. Регистрируем роутер в диспетчере
dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
4. Шаг 2: Профессиональная архитектура. Вынос логики в отдельный файл
Освоив механику Router
на простом примере, мы можем реализовать более надежную и масштабируемую архитектуру. В основе этого подхода лежит фундаментальный принцип разработки — Разделение Ответственности (Separation of Concerns). Суть его в том, что каждый компонент системы должен отвечать только за свою, четко определенную задачу.
В нашем случае:
Файл
main.py
будет выполнять роль точки входа: он будет отвечать только за инициализацию объектов (бот, диспетчер) и запуск приложения.Модули внутри папки
handlers/
будут содержать бизнес-логику: каждый файл будет отвечать за свой сценарий (например, обработка базовых команд, работа с заказами, функции администратора).
Давайте реализуем эту структуру.
1. Создание структуры проекта
Сначала приведем наш проект к следующему виду. Для этого создайте в корне вашего проекта папку handlers
, а внутри нее — файл basic_handlers.py
.
my_super_bot/
├── handlers/
│ └── basic_handlers.py # Файл для обработчиков базовых команд
└── main.py # Точка входа в приложение
2. Перенос логики в handlers/basic_handlers.py
Теперь вырежем весь код, связанный с определением роутера и его хэндлеров, из main.py
и перенесем его в наш новый файл handlers/basic_handlers.py
. Этот модуль будет полностью инкапсулировать логику обработки базовых команд.
# handlers/basic_handlers.py
from aiogram import Router, types
from aiogram.filters.command import Command
# Все роутеры нужно именовать так, чтобы не было конфликтов
router = Router()
@router.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Привет! Я бот, и моя логика вынесена в отдельный файл.")
@router.message()
async def echo_message(message: types.Message):
await message.answer(f"Я получил твое сообщение: {message.text}")
Обратите внимание, что этот файл теперь самодостаточен: он импортирует только те компоненты aiogram
, которые ему необходимы, и ничего не знает о Bot
или Dispatcher
.
3. Обновление main.py
В результате main.py
значительно упрощается. Его единственной задачей становится инициализация, импорт и регистрация нашего роутера.
# main.py
import asyncio
import logging
from aiogram import Bot, Dispatcher
# Импортируем наш роутер из пакета handlers
from handlers.basic_handlers import router
logging.basicConfig(level=logging.INFO)
# Убедитесь, что здесь ваш токен
bot = Bot(token="ВАШ_ТОКЕН_ЗДЕСЬ")
dp = Dispatcher()
async def main():
# Подключаем роутер к диспетчеру
dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Ключевое изменение здесь — from handlers.basic_handlers import router
. Мы импортируем уже настроенный экземпляр роутера и подключаем его к диспетчеру.
Теперь запустите бота снова командой python main.py
. Он работает так же, как и раньше, но теперь его архитектура стала чистой, модульной и готовой к добавлению нового, более сложного функционала.
Сохраните и запустите файл main.py
. Вы заметите, что поведение бота никак не изменилось: он по-прежнему отвечает на команду /start
и повторяет сообщения. Однако с точки зрения архитектуры мы сделали важный шаг: вся логика обработки сообщений теперь сгруппирована в объекте router
и готова к тому, чтобы быть вынесенной в отдельный модуль, чем мы и займемся на следующем шаге.
5. Шаг 3: Расширение функциональности. Фильтры для разных типов контента
Теперь, когда наша архитектура приведена в порядок, добавление новой функциональности становится простой и логичной задачей. Мы можем легко научить бота реагировать на разные типы контента, не затрагивая при этом main.py
. Для этого в aiogram
существует мощный и удобный инструмент — «магические фильтры».
Объект F
из библиотеки aiogram
— это конструктор, который позволяет декларативно описывать условия для срабатывания хэндлера. Вместо написания сложных lambda
-функций для проверки содержимого сообщения (например, lambda message: message.photo
), вы можете использовать более читаемый и лаконичный синтаксис: F.photo
.
Ключевое правило: Порядок регистрации хэндлеров
Прежде чем добавлять код, необходимо усвоить важнейшее правило: порядок регистрации хэндлеров в роутере имеет критическое значение. Диспетчер проверяет их последовательно, сверху вниз, и использует первый же обработчик, фильтры которого удовлетворяют входящему событию.
Это означает, что более специфичные хэндлеры должны всегда идти раньше более общих. Например, если обработчик для любого текста (F.text
) будет зарегистрирован раньше обработчика для команды (Command("start")
), то команда /start
никогда не будет обработана, так как она тоже является текстом и будет перехвачена первым, более общим хэндлером.
Правильный порядок:
Обработчики команд (
Command
).Обработчики специфичных типов контента (
F.photo
,F.sticker
,F.voice
).Обработчик любого текста (
F.text
).Обработчик любого сообщения (без фильтров).
Практическая реализация
Давайте обновим наш файл handlers/basic_handlers.py
, добавив в него логику для обработки фотографий и стикеров.
Импортируйте
F
: В самом верху файла добавьтеF
к остальным импортам изaiogram
.Добавьте новые хэндлеры: Напишем функции для реакции на фото и стикеры.
Организуйте хэндлеры в правильном порядке: Убедитесь, что обработчик текста стоит в самом конце.
Приведите код в файле handlers/basic_handlers.py
к следующему виду:
# handlers/basic_handlers.py
from aiogram import Router, types, F
from aiogram.filters.command import Command
router = Router()
# Хэндлер на команду /start (самый специфичный)
@router.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Привет! Отправь мне фото, стикер или голосовое.")
# Хэндлер на получение фото
@router.message(F.photo)
async def handle_photo(message: types.Message):
await message.answer("Отличное фото! Спасибо.")
# Хэндлер на получение стикера
@router.message(F.sticker)
async def handle_sticker(message: types.Message):
# message.sticker.file_id - можно получить id стикера
await message.answer("Милый стикер!")
# Хэндлер на любой текст (самый общий, должен быть последним)
@router.message(F.text)
async def handle_text(message: types.Message):
await message.answer(f"Ты прислал текст: {message.text}")
Теперь сохраните файл и запустите бота командой python main.py
. Протестируйте обновленную логику:
Отправьте команду
/start
— бот пришлет приветствие.Отправьте фотографию — бот ответит "Отличное фото! Спасибо.".
Отправьте стикер — бот отреагирует сообщением "Милый стикер!".
Отправьте обычный текст — бот ответит "Ты прислал текст: ...".
Такое предсказуемое поведение подтверждает, что наша архитектура и порядок хэндлеров настроены корректно.
6. Заключение и домашнее задание
В этом уроке мы совершили качественный скачок от простого скрипта к приложению с продуманной и масштабируемой архитектурой. Теперь ваш проект имеет прочный фундамент, на котором можно строить сложный и многофункциональный продукт.
Ключевые навыки, которые вы освоили:
Поняли проблему масштабирования и научились решать ее с помощью Роутеров (
Router
).Провели рефакторинг, вынеся логику обработки сообщений в отдельные, изолированные модули.
Освоили магические фильтры
F
, научив бота распознавать разные типы контента.На практике убедились, почему порядок регистрации хэндлеров критически важен для корректной работы бота.
Лучший способ закрепить теорию — это практика. Мы подготовили несколько заданий, которые помогут вам глубже понять пройденный материал и научиться самостоятельно модифицировать код. Все они выполняются в файловой структуре, которую мы создали на уроке. Нажмите на любое из заданий, чтобы раскрыть его условия.
Домашнее задание к Уроку 2
Задание 1: "Бот-меломан"
Что нужно сделать: Научите бота реагировать на отправленные ему аудиофайлы (музыку).
В файле
handlers/basic_handlers.py
добавьте новый хэндлер.Используйте магический фильтр
F.audio
, чтобы хэндлер срабатывал только на сообщения с аудио.В ответ на аудиофайл бот должен присылать сообщение вроде: "Отличный трек! Добавляю в свой плейлист."
Цель: Закрепить навык использования магических фильтров F
для новых типов контента.
Задание 2: "Сломанный бот" (намеренная ошибка)
Что нужно сделать: Это задание на понимание, почему порядок хэндлеров важен.
В файле
handlers/basic_handlers.py
поменяйте местами хэндлеры: обработчик текста (@router.message(F.text)
) поставьте самым первым, выше обработчика команды/start
.Запустите бота и отправьте ему команду
/start
.Напишите в комментарии к коду, что произошло и почему. (Подсказка: бот не отреагирует на команду так, как должен).
Верните хэндлер
F.text
на его законное место в конце файла, чтобы всё снова заработало.
Цель: На практике убедиться в критической важности правильного порядка расположения хэндлеров.
Задание 3: "Наводим порядок"
Что нужно сделать: Продолжаем улучшать архитектуру нашего проекта.
В папке
handlers
создайте новый файлmedia_handlers.py
.Перенесите из
basic_handlers.py
в новый файлmedia_handlers.py
всю логику, отвечающую за медиаконтент (хэндлеры для фото, стикеров и аудио из Задания 1). Не забудьте создать в новом файле свой роутер!В файле
main.py
импортируйте роутер изmedia_handlers.py
и тоже подключите его к диспетчеру. Теперь у вас будет подключено два роутера.
В итоге у вас должно получиться:
basic_handlers.py
содержит только обработку команд и текста.media_handlers.py
содержит только обработку фото, стикеров и аудио.main.py
подключает оба роутера.
Цель: Научиться создавать и подключать несколько роутеров из разных файлов, грамотно разделяя логику.
Задание 4: "Технический паспорт фото"
Что нужно сделать: Усовершенствуйте хэндлер для обработки фотографий в файле media_handlers.py
. Бот должен не просто отвечать "Отличное фото!", а присылать пользователю уникальный идентификатор (file_id
) этого фото.
Подсказка: Telegram отправляет фото в нескольких разрешениях. Они хранятся в message.photo
в виде списка. Чтобы получить file_id
самого качественного изображения, используйте конструкцию message.photo[-1].file_id
.
Ожидаемый результат:```
Пользователь: (отправляет фото)
Бот: Фото принято! Его file_id: AgACAgIAAxkBAA...
Цель: Научиться извлекать конкретные данные (не только текст) из объекта message
.
Задание 5 (со звездочкой): "Бот-охранник"*
Что нужно сделать: Напишите хэндлер, который будет срабатывать на любое текстовое сообщение, содержащее слово "дурак" (в любом регистре: "Дурак", "дУрАк" и т.д.), и отвечать на него "Пожалуйста, соблюдайте правила общения."
Подсказка: Можно использовать один магический фильтр F.text
и обычную проверку if
внутри функции. Но для настоящих профессионалов есть способ лучше: можно передать в декоратор вторым аргументом lambda
-функцию для более точной фильтрации.
Пример продвинутого фильтра:
@router.message(F.text, lambda message: "дурак" in message.text.lower())
Этот хэндлер нужно разместить до общего хэндлера на F.text
, но после хэндлеров на команды.
Цель: Познакомиться с возможностью создания более сложных, составных фильтров.
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!