Загрузка...

Aiogram. Разбираем FSM простыми словами

Тема в разделе Статьи создана пользователем gwaap 15 апр 2022. (поднята 2 сен 2024) 32 215 просмотров

  1. gwaap
    gwaap Автор темы 15 апр 2022 159 14 апр 2022
    Очень часто при написании определённого функционала, определённой цепочки действий, возникает необходимость как-то давать указания боту на что реагировать, а что пропускать мимо ушей. Что сохранять и как всё это совмещать.

    Всех приветствую дорогие форумчане! Сегодня мы поговорим про FSM (Finite State Machine), про машину состояний. Что это и как ей управляться. Давайте начнём.

    [IMG]

    Представим ситуацию. Вам нужно сделать бота для онлайн покупок. Вот вы написали функционал каталога, пришло время сделать самое основное. Функцию заказа. Вот пользователь вводит название товара, вы его сохраняется в переменную (не дай бог глобальную), далее вы предлагаете пользователю ввести свой адрес. И тут на тебе. А как? Как сделать так, чтобы после того как пользователь ввёл свой адрес, выполнился определённый handler?

    Тут к нам на помощь приходит FSM (Finite State Machine).

    Как вы могли понять, данная модель помогает передвигаться в определенные функции по скрипту, а также сохранять данные определённого пользователя, у которого активно FSM состояние.

    Чтобы начать работать с машиной состояний, для начала нам нужно инициализировать хранилище в dp (Dispatcher)
    Python
    from aiogram.contrib.fsm_storage.memory import MemoryStorage

    storage = MemoryStorage()
    dp = Dispatcher(bot, storage=storage)
    Отлично! Теперь мы можем записывать данные пользователя в оперативную память.

    Теперь давайте поговорим про состояния. Перед тем как создать какое-либо состояние, нам нужно создать класс, где мы поочередно опишем все states чтобы потом без проблем переключаться между ними. Возьмём пример с регистрацией пользователя и вводом его адреса. Создадим класс UserState.
    Python
    from aiogram.dispatcher.filters.state import StatesGroup, State


    class UserState(StatesGroup):
    name = State()
    address = State()
    Тут мы импортируем StatesGroup и State.

    После создания класса, мы можем приступать к самому интересному, к основному написанию скрипта. Так что же будет делать бот?

    1. спрашивать пользователя его имя
    2. записывать имя в переменную состояния
    3. переключаться на следующее состояние
    4. спрашивать пользователя его адрес
    5. записывать адрес в переменную состояния
    6. выводить полученную информацию о пользователе
    7. очищать состояние пользователя, а также все данные

    Для начала создадим handler, который будет обрабатывать команду /reg:
    Python
    @dp.message_handler(commands=['reg'])
    async def user_register(message: types.Message):
    await message.answer("Введите своё имя")
    await UserState.name.set()
    Тут у нас встречается подобная строчка
    await UserState.name.set()

    Мы обращаемся к классу UserState, далее к состоянию name, а методом set() мы устанавливаем данное состояние. Теперь у нас пользователь имеет состояние UserState.name и мы можем создать handler, который будет реагировать ТОЛЬКО на это состояние. Давайте же создадим такой handler в который пользователь будет записывать своё имя:

    Python
    @dp.message_handler(state=UserState.name)
    async def get_username(message: types.Message, state: FSMContext):
    await state.update_data(username=message.text)
    await message.answer("Отлично! Теперь введите ваш адрес.")
    await UserState.next() # либо же UserState.address.set()
    Итак, давайте разбираться:
    async def get_username(message: types.Message, state: FSMContext):

    Тут мы вторым аргументом передали state: FSMContext для того, чтобы мы могли записывать данные пользователя в память. Нам также необходимо импортировать:

    from aiogram.dispatcher import FSMContext


    Далее:
    await state.update_data(username=message.text)

    Здесь мы вызываем метод update_data(), который сохраняет данные в память, где единственным аргументом мы указываем название ключа в памяти 'username', а также его значение 'message.text'.

    Далее просим пользователя ввести его адрес, а после, неявно переключаем состояние на следующее (а следующий state у нас 'adress') с помощью метода

    await UserState.next()


    Однако переключайте состояния таким способом только тогда, когда точно знаете какой state (в классе) идёт дальше.

    Также можно переключать состояния явно:
    await
    UserState.address.set()


    Отлично:claps: Теперь создадим финальный хэндлер где будем получать адрес пользователя, а также выводить ему всю имеющуюся информацию.
    Python
    @dp.message_handler(state=UserState.address)
    async def get_address(message: types.Message, state: FSMContext):
    await state.update_data(address=message.text)
    data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
    f"Адрес: {data['address']}")

    await state.finish()
    В начале в принципе мы с вами уже всё разобрали, state, FSMContext, update_data. Но тут у нас появляется новый актер.
    data = await state.get_data()

    С помощью метода get_data() мы получаем все ранее записанные в состояние данные, и сохраняем всё в переменную data. Теперь мы можем получить данные состояний по ключу через переменную data. Что.. собственно мы и делаем в следующей строчке при выводе всей информации пользователю.
    Python
        data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
    f"Адрес: {data['address']}")
    И в конце концов мы всё завершаем методом finish()
    await state.finish()

    Данный метод очищает все состояния пользователя, а также удаляет все ранее сохраненные данные. Если же вам надо сбросить только состояние, воспользуйтесь:
    await state.reset_state(with_data=False)


    Весь код:
    Python
    from aiogram import types
    from aiogram.dispatcher import FSMContext
    from aiogram.dispatcher.filters.state import State, StatesGroup


    class UserState(StatesGroup):
    name = State()
    address = State()


    @dp.message_handler(commands=['reg'])
    async def user_register(message: types.Message):
    await message.answer("Введите своё имя")
    await UserState.name.set()


    @dp.message_handler(state=UserState.name)
    async def get_username(message: types.Message, state: FSMContext):
    await state.update_data(username=message.text)
    await message.answer("Отлично! Теперь введите ваш адрес.")
    await UserState.next() # либо же UserState.adress.set()


    @dp.message_handler(state=UserState.address)
    async def get_address(message: types.Message, state: FSMContext):
    await state.update_data(address=message.text)
    data = await state.get_data()
    await message.answer(f"Имя: {data['username']}\n"
    f"Адрес: {data['address']}")

    await state.finish()
    Итог
    [IMG]

    Друзья, надеюсь всем всё понятно разъяснил, если понравилась статья, дайте об этом знать:sobaken:

    На этом всё, всем удачи и светлого ума! До скорого.
     
    Этот материал оказался полезным?
    Вы можете отблагодарить автора темы путем перевода средств на баланс
    Отблагодарить автора
    15 апр 2022 Изменено
  2. AIOBOY
    AIOBOY 16 апр 2022 Заблокирован(а) 2 14 апр 2022
    [IMG]
    Найс, реализовал input() и print(), но только в телеге)
    Буду еще практиковаться. Спасибо за статью!
     
    1. Vodafit
      каким образом ты это сделал?
  3. N1K
    Давай ещё статей по aiogram, как раз его изучать начал)
     
  4. Sekkira
    Sekkira 7 май 2022 1 4 мар 2022
    :press_f: автор большой респект за статью! Все очень доступно!
     
  5. SenkoSan
    SenkoSan 29 май 2022 Заблокирован(а) 1040 8 окт 2021

    опечаточка. Метод set() в коде
     
    1. gwaap Автор темы
      SenkoSan, увидел:) спасибо, исправил.
  6. mynameisstem222
    лютый смак
    --- Сообщение объединено с предыдущим 2 июн 2022
    try:
    await message.answer(text)
    await state.finish() <-- это не останавливает принятие инфы




    except AttributeError:
    await message.answer("Что-то пошло не так! \n"
    "Попробуйте еще раз")

    await state.finish() <-- а тут все работает, как надо


    как заставить работать первый state.finish()?
     
    2 июн 2022 Изменено
    1. gwaap Автор темы
      mynameisstem222, ты в функцию state: FSMContext прописал?
  7. BarlCoder
    BarlCoder 11 июн 2022 Заблокирован(а) 3 8 июн 2022
    Лол каждый раз, когда работаю с фсм, захожу на эту статью)
     
  8. Shandeika
    Shandeika 22 июл 2022 Заблокирован 156 17 окт 2018
    Давай статью про aiogram 3.0, там немного поменяли все, поэтому людям будет полезно. Не все знают английский и могут прочитать документацию, так что написание статьи имеет смысл. https://docs.aiogram.dev/en/dev-3.x/dispatcher/finite_state_machine/index.html
     
    1. radix_rivers
      Shandeika, так это же бета еще
    2. Shandeika
      radix_rivers, но уже стабильно работает
  9. ucveoz
    ucveoz 15 авг 2022 Заблокирован(а) 1 25 июл 2022
    Как ждать файл? telegram: ucveoz пж
     
    1. gwaap Автор темы
      ucveoz, в хендлере пропиши фильтр content_types=[“document”] либо content_types=types.ContentType.DOCUMENT
  10. clipfailer
    clipfailer 17 сен 2022 Заблокирован(а) 47 5 июн 2020
    Я учу щас aiogram. Уроки проходят по модульной системе, то есть всё по разным файлам. Вот я по-твоему уроку сделал файл reg.py, в котором описана логика регистрации пользователя. Автор уроков сначала импортирует dp из основного файла в наш (reg.py), потом уже в __init__.py импортирует dp из reg.py. Я поверхностно понял, что мы просто получаем все dp из reg.py. Ну так вот, при запуске бота, когда я ввожу команду /reg, как и задумано, бот спрашивает моё имя, но как только я ввожу его, начинает работать другой файл, отвечающий за функцию echo, то есть бот просто отвечает моим именем. В имортах __init__.py echo файл должен импортироваться последним, чтобы не перебивать другие (как я понял), но при этом он получается все равно его перебивает. Может знаешь решение ?
     
    17 сен 2022 Изменено
    1. clipfailer
      clipfailer, Увидел твои новые уроки, так что ты уже ответил на мой вопрос. Спасибо!
  11. Ириска_неактив1019614
    Ириска_неактив1019614 26 сен 2022 Ничего не продаю / Ничего не скупаю 479 1 апр 2019
    Хорошее объяснение машины состояний. Мне друг по такому же принципу объяснял сцены в Node Js:claps::kitty:
     
  12. yasnouguaga
    так можно же просто создать функцию с хендлером, которая будет запрашивать адрес, вызываться она будет после запроса имени, в чем тогда прикол FSM, может я чего то не понимаю?
     
    3 окт 2022 Изменено
    1. badsnus
    2. aloaloalo12345
      yasnouguaga, так тебе же надо сохранить это состояние и передать дальше, типо если ты просто через хендлер это сделаешь, это никуда дальше не передается, а просто отображается как 1 сообщение
  13. neo_noir
    neo_noir 17 ноя 2022 1 4 сен 2022
    Статья выглядит годной, потому что тот же Груша расписал все куда заморочнее, чуть позже по твоему туториалу попробую сделать нужные себе функции и напишу получилось или нет
     
    1. neo_noir
      UPD: да, достаточно легко получилось, спасибо за статью. используя эту инфу и инфу из других источников можно запилить что требуется. Хотя конечно такой вопрос, я получил всю нужную инфу, она хранится в оперативке, а что с ней дальше делать? Могу ли я например попросить бота отправить всю эту информацию себе на почту, например? или на любую другую почту, которую я укажу в боте
  14. febqij
    febqij 28 ноя 2022 2 28 ноя 2022
    Спасибо большое за данную статью. Долго ломал голову с python-telegram-bot, а в aiogram все оказалось проще.
     
  15. na_official
    na_official 18 дек 2022 <script>alert`1`</script> 19 5 сен 2021
    Прикольно. Но чем это лучше обычного словаря типа
    states = {
    'tg_id': {...user state...}
    }?
    Все равно все поля определять надо. Только непонятно, зачем брать и разбирать решение от aiogram, если придумать собственное займет 2 минуты и его можно сделать гораздо функциональней.
     
    1. DEvgeniy
      na_official, сохранение информации и состояний в MemoryStorage и делается на словарях)
  16. Xliteee
    друзья как передать готовую перменную в машину состояний а не ждать ответа от пользователя?
     
    1. TiNOOREE
      Xliteee, Просто запиши сам вместо пользователя
      Python
      @dp.message_handler(state=UserState.name)
      async def get_username(message: types.Message, state: FSMContext):
      await state.update_data(username='ВАСЯ')
  17. FarCatch
    FarCatch 27 фев 2023 Заблокирован(а) 0 11 ноя 2021
    а вот как мне при команде админ, проверять айди пользователя и если оно верно писать ему какой либо текст
     
    1. json
      FarCatch, смотри, в типе Message есть свойство from_user.id (айди юзера который отправил месседж) и сделай проверку
  18. buruht32
    buruht32 17 мар 2023 0 11 мар 2023
    Статья очень полезная. Большое спасибо,сохранил в закладки.
     
  19. buruht32
    buruht32 22 мар 2023 0 11 мар 2023
    Привет. Написал опрос на основе этой машины. Но появилась небольшая проблема. Сделал запуск машины с помощью инлайн клавиатуры, всё работает нормально. Но первый запрос приходит как уведомление. Т есть сверху экрана и тут же исчезает. Делаешь ввод отправляешь,то следующее сообщение приходит по нормальному и машина выполняется как положено. Не могу найти ответ почему сообщение приходит сверху. Help!
     
    1. DEvgeniy
      buruht32, msg: types.CallbackQuery
      msg.message.answer - отправит сообщение
      msg.answer - отправит что то тип уведомления
    2. buruht32
      DEvgeniy, Спасибо,проблема уже решена. Правда другим способом. Пришлось добавить меню команд через BotFather,и теперь всё нормально отображается. А потом я уже нашёл ответ в каком то из видосов на Ютубе. Всё равно огромное спасибо что нашли время ответить на мой вопрос. Нюансов очень много и иногда приходится перелопатить много бесполезной инфы прежде чем отыщешь нужную.
  20. max7443
    max7443 26 мар 2023 0 26 мар 2023
    Вопрос: для каждого отдельного пользователя телеграмм свой state id? как этот механизм работает?
     
    1. DEvgeniy
      max7443, просмотри исходный код MemoryStorage. В документации устаревший исходник
Top
Загрузка...