Загрузка...

Aiogram. Parsing FSM in simple words

Thread in Articles created by gwaap Apr 15, 2022. (bumped Sep 2, 2024) 32,226 views

  1. gwaap
    gwaap Topic starter Apr 15, 2022 159 Apr 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:

    На этом всё, всем удачи и светлого ума! До скорого.
     
    This article was useful for you?
    You can thank the author of the topic by transferring funds to your balance
    Thank the author
  2. AIOBOY
    AIOBOY Apr 16, 2022 Banned 2 Apr 14, 2022
    [IMG]
    Найс, реализовал input() и print(), но только в телеге)
    Буду еще практиковаться. Спасибо за статью!
     
    1. Vodafit
      каким образом ты это сделал?
  3. N1K
    Давай ещё статей по aiogram, как раз его изучать начал)
     
  4. Sekkira
    Sekkira May 7, 2022 1 Mar 4, 2022
    :press_f: автор большой респект за статью! Все очень доступно!
     
  5. SenkoSan
    SenkoSan May 29, 2022 Banned 1040 Oct 8, 2021

    опечаточка. Метод set() в коде
     
    1. gwaap Topic starter
      SenkoSan, увидел:) спасибо, исправил.
    2. SenkoSan
      gwaap, Не за что. Тема топ)
  6. mynameisstem222
    лютый смак
    The post was merged to previous Jun 2, 2022
    try:
    await message.answer(text)
    await state.finish() <-- это не останавливает принятие инфы




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

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


    как заставить работать первый state.finish()?
     
    1. gwaap Topic starter
      mynameisstem222, ты в функцию state: FSMContext прописал?
  7. BarlCoder
    BarlCoder Jun 11, 2022 Banned 3 Jun 8, 2022
    Лол каждый раз, когда работаю с фсм, захожу на эту статью)
     
  8. Shandeika
    Shandeika Jul 22, 2022 Заблокирован 156 Oct 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 Aug 15, 2022 Banned 1 Jul 25, 2022
    Как ждать файл? telegram: ucveoz пж
     
    1. gwaap Topic starter
      ucveoz, в хендлере пропиши фильтр content_types=[“document”] либо content_types=types.ContentType.DOCUMENT
  10. clipfailer
    clipfailer Sep 17, 2022 Banned 47 Jun 5, 2020
    Я учу щас aiogram. Уроки проходят по модульной системе, то есть всё по разным файлам. Вот я по-твоему уроку сделал файл reg.py, в котором описана логика регистрации пользователя. Автор уроков сначала импортирует dp из основного файла в наш (reg.py), потом уже в __init__.py импортирует dp из reg.py. Я поверхностно понял, что мы просто получаем все dp из reg.py. Ну так вот, при запуске бота, когда я ввожу команду /reg, как и задумано, бот спрашивает моё имя, но как только я ввожу его, начинает работать другой файл, отвечающий за функцию echo, то есть бот просто отвечает моим именем. В имортах __init__.py echo файл должен импортироваться последним, чтобы не перебивать другие (как я понял), но при этом он получается все равно его перебивает. Может знаешь решение ?
     
    1. clipfailer
      clipfailer, Увидел твои новые уроки, так что ты уже ответил на мой вопрос. Спасибо!
  11. Ириска_неактив1019614
    Ириска_неактив1019614 Sep 26, 2022 Ничего не продаю / Ничего не скупаю 479 Apr 1, 2019
    Хорошее объяснение машины состояний. Мне друг по такому же принципу объяснял сцены в Node Js:claps::kitty:
     
  12. yasnouguaga
    так можно же просто создать функцию с хендлером, которая будет запрашивать адрес, вызываться она будет после запроса имени, в чем тогда прикол FSM, может я чего то не понимаю?
     
    1. badsnus
    2. aloaloalo12345
      yasnouguaga, так тебе же надо сохранить это состояние и передать дальше, типо если ты просто через хендлер это сделаешь, это никуда дальше не передается, а просто отображается как 1 сообщение
  13. neo_noir
    neo_noir Nov 17, 2022 1 Sep 4, 2022
    Статья выглядит годной, потому что тот же Груша расписал все куда заморочнее, чуть позже по твоему туториалу попробую сделать нужные себе функции и напишу получилось или нет
     
    1. neo_noir
      UPD: да, достаточно легко получилось, спасибо за статью. используя эту инфу и инфу из других источников можно запилить что требуется. Хотя конечно такой вопрос, я получил всю нужную инфу, она хранится в оперативке, а что с ней дальше делать? Могу ли я например попросить бота отправить всю эту информацию себе на почту, например? или на любую другую почту, которую я укажу в боте
  14. febqij
    febqij Nov 28, 2022 2 Nov 28, 2022
    Спасибо большое за данную статью. Долго ломал голову с python-telegram-bot, а в aiogram все оказалось проще.
     
  15. na_official
    na_official Dec 18, 2022 <script>alert`1`</script> 19 Sep 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 Feb 27, 2023 Banned 0 Nov 11, 2021
    а вот как мне при команде админ, проверять айди пользователя и если оно верно писать ему какой либо текст
     
    1. json
      FarCatch, смотри, в типе Message есть свойство from_user.id (айди юзера который отправил месседж) и сделай проверку
  18. buruht32
    buruht32 Mar 17, 2023 0 Mar 11, 2023
    Статья очень полезная. Большое спасибо,сохранил в закладки.
     
  19. buruht32
    buruht32 Mar 22, 2023 0 Mar 11, 2023
    Привет. Написал опрос на основе этой машины. Но появилась небольшая проблема. Сделал запуск машины с помощью инлайн клавиатуры, всё работает нормально. Но первый запрос приходит как уведомление. Т есть сверху экрана и тут же исчезает. Делаешь ввод отправляешь,то следующее сообщение приходит по нормальному и машина выполняется как положено. Не могу найти ответ почему сообщение приходит сверху. Help!
     
    1. DEvgeniy
      buruht32, msg: types.CallbackQuery
      msg.message.answer - отправит сообщение
      msg.answer - отправит что то тип уведомления
    2. buruht32
      DEvgeniy, Спасибо,проблема уже решена. Правда другим способом. Пришлось добавить меню команд через BotFather,и теперь всё нормально отображается. А потом я уже нашёл ответ в каком то из видосов на Ютубе. Всё равно огромное спасибо что нашли время ответить на мой вопрос. Нюансов очень много и иногда приходится перелопатить много бесполезной инфы прежде чем отыщешь нужную.
  20. max7443
    max7443 Mar 26, 2023 0 Mar 26, 2023
    Вопрос: для каждого отдельного пользователя телеграмм свой state id? как этот механизм работает?
     
    1. DEvgeniy
      max7443, просмотри исходный код MemoryStorage. В документации устаревший исходник
Top
Loading...