Опросы в Telegram-боте с помощью AIOGram с записью в JSON

Опросы в Telegram-боте с помощью AIOGram с записью в JSON

Сегодня мы разберем как сделать опросник в Telegram и запустить сбор данных о пользователях. Чтобы не повторяться, буду иногда обращаться к предыдущим статьям. 

Для создания опросника мы воспользуемся библиотекой AIOGram. 
Результаты опросов будем сохранять в файл JSON. 

Что потребуется для создания опросника: 

  • Компьютер или ноутбук (для особых извращений можно и телефон)
  • Редактор кода (У меня PyCharm)
  • Python версии 3.9 и выше
  • Соединение с интернетом

Установка библиотек и структура проекта

Установка AIOGram для Windows:

pip install aiogram json

AIOGram для macOS:

pip3 install aiogram json

Пользоваться будем уже известным шаблоном для разработки бота
При разработке подобных проектов шаблон сильно упростит вам жизнь.

Структура проекта:

+---telegram_bot
|   +---handlers
|   |  +---Users
|   |   |   +---__init__.py
|   |  |   +---help.py
|   |  |   +---audio.py
|   |  |   \---start.py
|   |  \---__init__.py
|   +---states
|   |  +---__init__.py
|   |  \---dowload.py
|   +---keyboards
|   |  |   +---inline
|   |   |   |---choice_but_start_test.py
|   |   |   \---__init__.py
|   |  \---__init__.py
|   +---utils
|   |  +---__init__.py
|   |  \---set_bot_commands.py
|   +---app.py
|  \---loader.py

Как и в прошлом разборе, мы будем двигаться по всем директориям и рассматривать написанный код. 

Разбор кода для создания опросника

Loader.py

from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage

token = '52627**************HQvtGZWe_BTVyKi4H1FvT_ezSCy8'

bot = Bot(token=token, parse_mode=types.ParseMode.HTML)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)

Импортируем класс бота, диспетчера и типы, а также класс для хранения информации в оперативной памяти.

Дальше сохраняем в переменную токен (как его получить смотрите тут), после чего инициализируем бота и диспетчера. 

Этот файл будет мотором нашего бота. В дальнейшем мы будем обращаться сюда, чтобы расширять наши возможности.

Анонсы всех видео, статей и полезностей - в нашем Telegram-канале🔥
Присоединяйтесь, обсуждайте и автоматизируйте!

App.py

Как и в прошлом гайде, этот файл будет собирать все хендлеры и инициализировать их, чтобы потом запускать прием от API:

from aiogram import executor

from loader import dp
import handlers
from utils.set_bot_commands import set_default_commands

async def on_startup(dispatcher):
    # Устанавливаем дефолтные команды
    await set_default_commands(dispatcher)

if __name__ == '__main__':
    executor.start_polling(dp, on_startup=on_startup)

Utils

Set_bot_commands.py

В этом файле мы создаем команды, которые отображаются в меню. Первый аргумент - вызов команды, второй - её описание:

from aiogram import types

async def set_default_commands(dp):
    await dp.bot.set_my_commands(
        [
            types.BotCommand("start", "Запустить бота"),
            types.BotCommand("help", "Вывести справку"),
            types.BotCommand("onstarttest", "Пройти первый опрос"),
        ]
    )

 

States

__init__.py

from .on_start_test import CallbackOnStart

Подробнее о пакетах в python: ссылка на статью, ссылка на видео. 
Для представления о пакетах этих ссылок хватит.

On_start_test.py

В этом файле будут храниться наши состояния, в которые мы будем сохранять информацию на время опроса. Подробнее можно узнать о машинном состояние по запросу FSM.

У нас будет три состояния для трех этапов опроса:

from aiogram.dispatcher.filters.state import StatesGroup, State

class CallbackOnStart(StatesGroup):
    Q1 = State()
    Q2 = State()
    Q3 = State()

Keyboards 

Чтобы не захламлять основной файл, будем выносить все кнопки в отдельный пакет. В нашем случае используем генератор для inline кнопок.

__init__.py 

Импортируем типы для работы с кнопками. Дальше пишем функцию создания кнопок из массива. У нас будет двумерный массив - каждый массив с городами будет в одной линии у пользователя: 

from . import inline
choice_but_start_test.py
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton

def towers():
    list_button_name = [['Москва', 'Санкт Петербург', 'Нижний Новгород', 'Ростов'],
                        ['Новосибирск', 'Екатеринбург', 'Казань', 'Челябинск']]

    buttons_list = []
    for item in list_button_name:
        l = []
        for i in item:
            l.append(InlineKeyboardButton(text=i, callback_data=i))
        buttons_list.append(l)

    keyboard_inline_buttons = InlineKeyboardMarkup(inline_keyboard=buttons_list)
    return keyboard_inline_buttons

Почему именно так? Если не прибегать к генератору кнопок, нам бы пришлось каждую кнопку прописывать руками, а это неудобно. 

Подробнее о кнопках в AIOGram можно узнать тут
Функция возвращает массив кнопок для работы с ними.

Handlers

On_start_testing.py

Это лишь пример. Вы всегда можете переделать его под себя, чтобы получать нужные вам данные от пользователей.

Импорты

Все импорты понятные, но сделаем акцент на библиотеке json. Она нужна для взаимодействия с файлами JSON, в них мы будем сохранять результаты опроса.

import json

from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters import Command
from aiogram.types import ReplyKeyboardRemove

from keyboards.inline.choice_but_start_test import towers

from loader import dp
from aiogram import types
from states import CallbackOnStart

Реакция на команду

Этот хендлер будет отзываться на команду onstarttest.

Первым делом мы проверяем проходил ли пользователь опрос раньше. Для этого открываем базу данных в JSON-файле и перебираем все элементы в поисках ID пользователя. Если его нет, запускаем тест, переводя пользователя в FSM. Если пользователь уже есть в базе - отправляем сообщение, что он проходил опрос ранее.

@dp.message_handler(Command('onstarttest'))
async def on_start_test(message: types.Message):
    id = message.from_user.id
    with open('users_test_one.json', encoding='utf-8') as json_file:
        data = json.load(json_file)
        for i in data:
            if int(i) == id:
                user = False
                break
            else:
                user = True
    if user:
        await message.answer("Описание опросника")
        await message.answer('Вопрос №1\nСколько вам лет?\nНапишите ответ (только число)', reply_markup=ReplyKeyboardRemove())
        await CallbackOnStart.Q1.set()
    else:
        await message.answer(text="Вы уже проходили тест")

Следующий хендлер срабатывает при состоянии Q1. Впоследствии генерируется набор кнопок. После ответа пользователя, полученные данные сохраняются в FSM кэш. После всех действий бот отправляет новый вопрос и переводит человека в новую фазу состояния:

@dp.message_handler(state=CallbackOnStart.Q1)
async def tower(message: types.Message, state: FSMContext):
    b = towers()
    answer = message.text
    await state.update_data(name=answer)
    await message.answer(text="Вопрос №2\nВ каком городе вы живете?\nВыберите ответ из предложенных",
                         reply_markup=b)
    await CallbackOnStart.next()

Answer - это данные из скрытого ответа inline кнопки. Data запрашивает все данные, сохранённые в FSM. Дальше мы готовим информацию к сохранению в JSON-файл (под ID пользователя мы добавляем полученные ответы). Когда всё готово, отправляем пользователю его выбранные ответы:

@dp.callback_query_handler(state=CallbackOnStart.Q2)
async def end(call: types.Message, state: FSMContext):
    answer = call.data
    await state.update_data(full_name=call.from_user.full_name)
    await state.update_data(repost=answer)
    data = await state.get_data()
    user = {call.from_user.id: data}
    text = []
    for i in data:
        text.append(f'{data[i]}\n')
    await call.message.answer(text="Ваши ответы:", reply_markup=ReplyKeyboardRemove())
    await call.message.answer('\n'.join(text))
    with open('users_test_one.json', encoding='utf-8') as file:
        data = json.load(file)
        data.update(user)
        with open('users_test_one.json', 'w', encoding='utf-8') as outfile:
            json.dump(data, outfile, indent=4, ensure_ascii=False)
    await state.finish()

Подробнее о сохранении в файл 

Подразумевается, что файл будет создан заранее. В противном случае python выведет ошибку о его отсутствии, поскольку режим чтения файла в автоматическом режиме стоит "r" (подробнее о режимах тут): 

with open('users_test_one.json', encoding='utf-8') as file:
    data = json.load(file)
    data.update(user)
    with open('users_test_one.json', 'w', encoding='utf-8') as outfile:
        json.dump(data, outfile, indent=4, ensure_ascii=False)

Конструкцией "with open as file" мы сразу присваиваем открытому файлу название и можем удобно с ним работать. Дальше мы скармливаем библиотеке JSON-файл, чтобы она конвертировала его в словарь языка. К старым данным внутри файла добавляются новые с помощью метода "update". 

После всех манипуляций мы еще раз открываем файл с пометкой w (открытие файла для записи) и перезаписываем его с дополнениями. 

Для корректного сохранения используем библиотеку json и функцию dump. Первый аргумент dump - что записываем, второй - куда записываем (это должен быть уже открытый файл), а третий - размер отступов внутри.

Заключение

Сегодня мы познакомились с новыми возможностями AIOGram и сделали на его базе опросник, который собирает данные пользователей в JSON-файл. 

Вы можете применять такой опросник в том числе для своего бизнеса и исследований вашей аудитории, собирая и анализируя ответы пользователей. 

При должном желании, можно использовать части кода или идеи в других проектах.

Полезные ссылки

Документация AIOGram: https://docs.aiogram.dev/en/latest/

GitHub всего проекта: https://github.com/Redkomel56/aiogram_survey

Комментарии