Как организовать последовательный опрос пользователя в Telegram-боте на 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, чтобы состояния пользователей не терялись.

Ответ 18+ 🔞

А, вот эта тема про диалоги в ботах! Ну, блядь, классика же, каждый второй разработчик на этом спотыкается, как пьяный о бордюр. Слушай, сейчас разжую, как бабушка манную кашу.

Так вот, смотри. Чтобы бот не был тупым, как пробка, и мог вести нормальный диалог — типа «спросил имя, спросил возраст, запомнил всё это дело» — нужна какая-то память. А то он тебе на втором вопросе уже забудет, что ты на первый ответил, и будет как тот Герасим из «Муму» — мычит одно и то же.

Для этого в aiogram есть такая штука — машина состояний, или FSM. Это не «Фонд Свободных Макдаков», а Finite State Machine. Суть в том, что бот запоминает, на каком шаге диалога ты сейчас находишься. Представь себе анкету в военкомате: пока не заполнишь первую страницу, на вторую не пустят. Вот тут так же.

Как это работает, ёпта:

  1. Придумываем шаги. Создаёшь класс, где каждый шаг диалога — это отдельное «состояние». Как очередь в поликлинике: «запись», «кабинет врача», «аптека».
  2. Ставим в очередь. Когда пользователь начинает диалог, бот ему говорит: «Ты теперь в состоянии „ждать имени“». И ставит его в эту виртуальную очередь.
  3. Ловим по шагам. Пишем обработчики, которые срабатывают только если пользователь находится в конкретном состоянии. Не перепутает имя с возрастом.
  4. Сохраняем и двигаем дальше. Получил ответ — сохранил его в карман (во временное хранилище) и перевёл бедолагу на следующий шаг.
  5. Выпускаем с миром. Когда все вопросы кончились — собрал все ответы из кармана, отдал результат и сбросил состояние. Человек свободен!

Смотри, как это в коде выглядит, блядь:

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("Окей. А лет сколько отроду?")

# Ловим на втором шаге
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("Ясненько. И из какого ты города, красавец?")

# Финальный аккорд
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. Чтобы состояния переживали перезапуски и не терялись, как носки в стирке.