Как организовать последовательный опрос пользователя в Telegram-боте на Python?

«Как организовать последовательный опрос пользователя в Telegram-боте на Python?» — вопрос из категории Архитектура, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для организации последовательных диалогов (опросов, анкет) в Telegram-ботах на Python используется механизм машины состояний (Finite State Machine, FSM).

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

Принцип работы:

  1. Определение состояний: Создается класс, наследуемый от StatesGroup, где каждое состояние — это шаг в диалоге.
  2. Установка состояния: При старте диалога бот устанавливает пользователю начальное состояние (например, await Form.question1.set()).
  3. Обработка по состоянию: Создаются хендлеры, которые срабатывают только для пользователей в определенном состоянии.
  4. Сохранение данных и переход: Внутри хендлера ответ пользователя сохраняется во временное хранилище (state.proxy()) и бот переводит пользователя в следующее состояние (await Form.next()).
  5. Завершение: После последнего ответа состояние сбрасывается (await state.finish()).

Пример на aiogram:

from aiogram import Bot, Dispatcher, types
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram.contrib.fsm_storage.memory import MemoryStorage

# 1. Определяем состояния
class UserPoll(StatesGroup):
    name = State()      # Состояние для ожидания имени
    age = State()       # Состояние для ожидания возраста
    city = State()      # Состояние для ожидания города

# Хендлер для начала опроса
async def start_poll(message: types.Message):
    await UserPoll.name.set() # 2. Устанавливаем первое состояние
    await message.answer("Как вас зовут?")

# 3. Хендлер, который ловит ответ, когда пользователь в состоянии 'name'
async def process_name(message: types.Message, state: FSMContext):
    async with state.proxy() as data: # 4. Сохраняем данные
        data['name'] = message.text
    await UserPoll.next() # Переходим к следующему состоянию (age)
    await message.answer("Сколько вам лет?")

# Хендлер для состояния 'age'
async def process_age(message: types.Message, state: FSMContext):
    async with state.proxy() as data:
        data['age'] = message.text
    await UserPoll.next() # Переходим к следующему состоянию (city)
    await message.answer("Из какого вы города?")

# Хендлер для последнего состояния 'city'
async def process_city(message: types.Message, state: FSMContext):
    async with state.proxy() as data:
        data['city'] = message.text
        # Выводим собранные данные
        await message.answer(f"Спасибо! Ваши данные: Имя - {data['name']}, Возраст - {data['age']}, Город - {data['city']}")

    await state.finish() # 5. Завершаем диалог и сбрасываем состояние

# Регистрация хендлеров
def register_handlers(dp: Dispatcher):
    dp.register_message_handler(start_poll, commands=['poll'], state=None)
    dp.register_message_handler(process_name, state=UserPoll.name)
    dp.register_message_handler(process_age, state=UserPoll.age)
    dp.register_message_handler(process_city, state=UserPoll.city)

Важно: По умолчанию aiogram использует хранилище в памяти (MemoryStorage), которое сбрасывается при перезапуске бота. Для продакшена следует использовать внешние хранилища, такие как RedisStorage или MongoStorage, чтобы состояния пользователей не терялись.