Опрос в Телеграм-боте на Python Aiogram с записью в JSON

Опрос в Телеграм-боте на Python Aiogram с записью в JSON

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

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

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

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

Как работает опрос в Телеграм-боте

Перед созданием кода разберём логику. Опрос в Telegram-боте - это последовательность вопросов, на которые пользователь отвечает по очереди. Бот запоминает где находится диалог (на каком вопросе) и куда складывать ответы.

Есть два способа сделать опрос на Python через aiogram:

  • Свой опросник через FSM - бот сам задаёт вопросы текстом или кнопками, ведёт пользователя по шагам и сохраняет ответы. Гибкий вариант: любые типы вопросов, своя логика, запись в JSON или базу. Этим и займёмся.
  • Нативный Telegram Poll - встроенный опрос Telegram (send_poll), который рисует красивый виджет с вариантами и сам считает голоса. Удобно для быстрых голосований, но негибко. Разберём его отдельно ниже.

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

Ключевой механизм - FSM (машина состояний). Она запоминает на каком вопросе находится каждый пользователь, чтобы бот не путал ответы и вёл всех параллельно.

Установка aiogram и структура проекта для опросника

Ставим библиотеку. Нужен только aiogram - модуль json встроен в Python, его отдельно ставить не нужно.

pip install aiogram

В отличие от старого подхода со структурой из десятка файлов, на aiogram 3 опросник удобно собрать в один файл. Так проще разобраться, а разбить на модули можно потом.

Также понадобится токен бота - получите его у @BotFather в Telegram через команду /newbot.

Пишем опрос в Telegram-боте на aiogram: разбор кода

Соберём опросник на aiogram 3. Бот спросит имя, возраст и любимый язык программирования, а ответы сохранит в JSON.

Импорты и инициализация:

import asyncio
import json
import os
from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

BOT_TOKEN = "ВАШ_ТОКЕН_ОТ_BOTFATHER"
RESULTS_FILE = "results.json"

bot = Bot(token=BOT_TOKEN)
dp = Dispatcher(storage=MemoryStorage())

Описываем шаги опроса через FSM-состояния. Каждый вопрос - отдельное состояние:

class Survey(StatesGroup):
    name = State()
    age = State()
    language = State()

Функция для записи результата в JSON. Читаем существующий файл, добавляем новый ответ, сохраняем обратно:

def save_to_json(data: dict):
    results = []
    if os.path.exists(RESULTS_FILE):
        with open(RESULTS_FILE, "r", encoding="utf-8") as f:
            results = json.load(f)
    results.append(data)
    with open(RESULTS_FILE, "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)

ensure_ascii=False нужен чтобы русский текст в JSON сохранялся читаемо, а не в виде кодов. indent=2 делает файл человекочитаемым.

Запуск опроса по команде /start:

@dp.message(CommandStart())
async def start_survey(message: Message, state: FSMContext):
    await state.set_state(Survey.name)
    await message.answer("Привет! Давай пройдём опрос. Как тебя зовут?")

Обработчики каждого шага. Бот сохраняет ответ в FSM-данные и переходит к следующему вопросу:

@dp.message(Survey.name, F.text)
async def process_name(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    await state.set_state(Survey.age)
    await message.answer("Сколько тебе лет?")


@dp.message(Survey.age, F.text)
async def process_age(message: Message, state: FSMContext):
    await state.update_data(age=message.text)
    await state.set_state(Survey.language)
    await message.answer("Какой язык программирования любишь?")


@dp.message(Survey.language, F.text)
async def process_language(message: Message, state: FSMContext):
    await state.update_data(language=message.text)
    data = await state.get_data()
    data["user_id"] = message.from_user.id
    save_to_json(data)
    await message.answer("Спасибо! Опрос завершён, ответы сохранены.")
    await state.clear()

И запуск бота:

async def main():
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Как это работает: команда /start ставит первое состояние и задаёт вопрос. Каждый обработчик ловит ответ для своего состояния через F.text, сохраняет его в данные FSM и переключает на следующий шаг. На последнем шаге всё пишется в JSON и состояние сбрасывается через state.clear().

Результат в results.json выглядит так:

[
  {
    "name": "Иван",
    "age": "25",
    "language": "Python",
    "user_id": 123456789
  }
]

Варианты ответов через инлайн-кнопки

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

[TGBLOCK]

Добавим выбор языка кнопками:

from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery

def language_keyboard():
    langs = ["Python", "JavaScript", "Go", "Rust"]
    buttons = [[InlineKeyboardButton(text=l, callback_data=f"lang:{l}")] for l in langs]
    return InlineKeyboardMarkup(inline_keyboard=buttons)


@dp.callback_query(F.data.startswith("lang:"))
async def process_lang_button(call: CallbackQuery, state: FSMContext):
    lang = call.data.split(":")[1]
    await state.update_data(language=lang)
    data = await state.get_data()
    data["user_id"] = call.from_user.id
    save_to_json(data)
    await call.message.answer(f"Выбран {lang}. Опрос завершён!")
    await state.clear()
    await call.answer()

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

Проверка и валидация ответов

Пользователь не всегда отвечает так, как мы ждём. На вопрос про возраст можно прислать «много», на число - текст, а то и стикер. Если не проверять ввод, бот сохранит мусор или упадёт с ошибкой. Добавим валидацию.

Проверим, что возраст - это число от 1 до 120. Если нет - бот переспросит, не двигая опрос дальше:

@dp.message(Survey.age, F.text)
async def process_age(message: Message, state: FSMContext):
    if not message.text.isdigit() or not (1 <= int(message.text) <= 120):
        await message.answer("Введите возраст числом, например 25.")
        return
    await state.update_data(age=int(message.text))
    await state.set_state(Survey.language)
    await message.answer("Какой язык программирования любишь?")

Ключевой приём - ранний выход через return. Пока ответ не прошёл проверку, мы не меняем состояние FSM, поэтому пользователь остаётся на том же вопросе и отвечает заново.

Отдельно стоит ловить ответы не текстом. Если на текстовый вопрос прислали фото или стикер, фильтр F.text его не пропустит, и сообщение останется без ответа. Добавим вежливую подсказку:

@dp.message(Survey.name)
async def name_wrong_type(message: Message):
    await message.answer("Пожалуйста, напишите ответ текстом.")

Этот обработчик ставим после основного process_name. Он сработает, только если первый не подошёл - то есть когда пришёл не текст. Так опрос в Телеграме становится устойчивым к любому вводу и не ломается на живых пользователях.

Ветвление: разные вопросы по условию

В живых опросах следующий вопрос часто зависит от предыдущего ответа. Спросили «Пишете на Python?» - если да, уточняем фреймворк, если нет, спрашиваем про другой язык. Такое ветвление - сильная сторона своего опросника на aiogram, нативный Telegram Poll так не умеет.

Логика простая: внутри обработчика смотрим ответ и выбираем, в какое состояние перейти:

@dp.message(Survey.knows_python, F.text)
async def branch_python(message: Message, state: FSMContext):
    answer = message.text.strip().lower()
    await state.update_data(knows_python=answer)
    if answer in ("да", "yes", "ага"):
        await state.set_state(Survey.framework)
        await message.answer("Отлично! Какой фреймворк используете?")
    else:
        await state.set_state(Survey.other_lang)
        await message.answer("Понятно. А на чём тогда пишете?")

Дальше каждая ветка ведёт в свой обработчик и свой вопрос. В конце ветки сходятся: оба пути сохраняют результат в JSON и завершают опрос через state.clear().

Так из линейной анкеты получается сценарий, который подстраивается под человека. Можно строить квизы с подсчётом баллов, многошаговые формы заявки или диалоги с пропуском ненужных вопросов - всё это один и тот же приём с FSM-состояниями.

Нативный опрос Telegram через send_poll

Если нужен быстрый опрос-голосование без сбора в свой файл, у Telegram есть встроенный виджет опроса. Бот отправляет его одной командой:

from aiogram.filters import Command

@dp.message(Command("poll"))
async def send_native_poll(message: Message):
    await bot.send_poll(
        chat_id=message.chat.id,
        question="Какой язык программирования лучше для старта?",
        options=["Python", "JavaScript", "Go"],
        is_anonymous=False,
    )

Telegram сам нарисует красивый опрос, посчитает голоса и покажет проценты. Можно сделать режим квиза с правильным ответом:

await bot.send_poll(
    chat_id=message.chat.id,
    question="Какой тип данных в Python неизменяемый?",
    options=["list", "tuple", "dict"],
    type="quiz",
    correct_option_id=1,
    is_anonymous=False,
)

Когда что выбирать: нативный Telegram Poll - для быстрых голосований и квизов, где не нужно хранить ответы у себя. Свой опросник на FSM с записью в JSON - когда нужны анкеты, кастомная логика и доступ к собранным данным.

Хранение результатов опроса: JSON и дальше

JSON в Python - простой и популярный способ хранить данные опроса. Разберём пару полезных приёмов.

Чтение всех результатов:

with open("results.json", "r", encoding="utf-8") as f:
    results = json.load(f)

print(f"Всего ответов: {len(results)}")
for r in results:
    print(r["name"], r["language"])

Простая аналитика - какой язык популярнее:

from collections import Counter

languages = [r["language"] for r in results]
print(Counter(languages))
# Counter({'Python': 5, 'Go': 2, 'JavaScript': 1})

JSON удобен пока данных немного - сотни и тысячи ответов. Когда опрос популярный и записей становятся десятки тысяч, JSON-файл начинает тормозить (его приходится читать целиком на каждую запись). Тогда стоит перейти на базу данных - SQLite для простых случаев или MongoDB, если структура ответов гибкая.

Важный нюанс безопасности: если несколько пользователей отвечают одновременно, запись в один JSON-файл может привести к гонке данных. Для нагруженного бота используйте базу данных или блокировки записи.

Экспорт результатов опроса в CSV и Excel

JSON удобен боту, но для анализа результаты опроса чаще нужны в таблице - открыть в Excel, посчитать, построить графики. Переложим данные из JSON в CSV: модуль csv, как и json, встроен в Python.

import csv
import json

with open("results.json", "r", encoding="utf-8") as f:
    results = json.load(f)

with open("results.csv", "w", encoding="utf-8-sig", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "age", "language", "user_id"])
    writer.writeheader()
    writer.writerows(results)

Тонкость для Excel - кодировка utf-8-sig. Обычный utf-8 Excel открывает с кракозябрами вместо русских букв, а версия с BOM (utf-8-sig) показывает кириллицу корректно. Параметр newline="" убирает лишние пустые строки между записями.

Можно сделать и команду выгрузки прямо в боте - администратор пишет /export, а бот присылает готовый файл:

from aiogram.types import FSInputFile

@dp.message(Command("export"))
async def export_csv(message: Message):
    document = FSInputFile("results.csv")
    await message.answer_document(document, caption="Результаты опроса")

Теперь собранные данные не заперты в JSON: пара строк - и опрос из Телеграм-бота превращается в таблицу для аналитики.

Деплой опросного бота на сервер

Локально бот работает пока включён компьютер. Для постоянной работы нужен сервер - простого VDS за 300-400 рублей в месяц (например, Hostland) хватит.

Через systemd на Linux:

[Unit]
Description=Telegram survey bot
After=network.target

[Service]
WorkingDirectory=/opt/survey_bot
ExecStart=/usr/bin/python3 /opt/survey_bot/bot.py
Restart=always

[Install]
WantedBy=multi-user.target

Сохраняем как /etc/systemd/system/survey_bot.service, запускаем systemctl enable --now survey_bot. Бот сам перезапускается при падении и ребуте сервера.

Важно: файл results.json с ответами лежит в рабочей директории бота. Не забывайте про бэкапы - если сервер переустановят, собранные данные пропадут.

Частые ошибки при создании опроса в Телеграме

Собрал грабли, на которые чаще всего наступают, когда делают опрос в Телеграм-боте на aiogram. Зная их заранее, сэкономите часы отладки.

Бот не реагирует на ответы в опросе. Почти всегда причина - забытое состояние. Если в обработчике не указать состояние (Survey.name), он не привяжется к шагу опроса и не сработает. Проверьте, что у каждого шага есть свой фильтр состояния и вы реально переключаете состояние через set_state.

Перепутан порядок обработчиков. aiogram перебирает обработчики сверху вниз и берёт первый подходящий. Если общий обработчик (без состояния) стоит выше конкретного, он перехватит сообщение, и шаг опроса не отработает. Частные обработчики - выше, общие - ниже.

Ответы перезаписывают друг друга. Если несколько пользователей проходят опрос одновременно, а вы храните данные в обычной переменной вместо FSM, ответы смешаются. Именно для этого нужен FSM: он держит данные каждого пользователя отдельно.

Кракозябры в JSON. Забыли ensure_ascii=False - и русские ответы сохраняются кодами вроде Иван. Файл валиден, но читать невозможно. Всегда ставьте этот параметр для опросов на русском.

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

Бот падает на нестандартном вводе. Без валидации любой неожиданный ответ (пустое сообщение, эмодзи, слишком длинный текст) способен уронить опрос. Проверяйте ввод - как в разделе про валидацию выше.

Куда развивать опросного бота на aiogram

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

  • Несколько опросов сразу - храните для каждого опроса свой файл результатов и свою группу состояний, а выбор опроса выводите кнопками.
  • Ограничение «один ответ на пользователя» - перед стартом проверяйте user_id в сохранённых результатах.
  • Напоминания - через планировщик APScheduler бот сам присылает опрос по расписанию.
  • Админ-панель - команды для просмотра статистики и выгрузки результатов прямо из чата.
  • База данных вместо JSON - когда опрос в Телеграме становится популярным, переносите хранение на SQLite или MongoDB.

Каждый пункт - это надстройка над тем же FSM-движком: добавляете состояния, обработчики и кнопки. Так из простого опросника вырастает полноценный бот сбора данных на aiogram.

FAQ: опрос в Телеграме на Python

Как сделать опрос в Telegram-боте на Python?

Два способа: свой опросник через aiogram и FSM-состояния (бот ведёт пользователя по вопросам и сохраняет ответы) или нативный Telegram Poll через send_poll. Для анкет с записью данных подходит первый, для быстрых голосований - второй.

Зачем использовать FSM для опроса?

FSM (машина состояний) запоминает на каком вопросе находится каждый пользователь. Без неё бот не понимал бы, к какому вопросу относится ответ, и путал бы данные разных людей. В aiogram FSM встроен.

Как сохранить ответы опроса в JSON?

Через модуль json (встроен в Python): читаем существующий файл через json.load, добавляем новый ответ в список, сохраняем обратно через json.dump с параметром ensure_ascii=False для читаемого русского текста.

Чем нативный Telegram Poll отличается от своего опросника?

Нативный Poll (send_poll) - готовый виджет Telegram с подсчётом голосов, но негибкий. Свой опросник на aiogram даёт полный контроль: любые типы вопросов, своя логика, сохранение данных куда угодно.

Когда переходить с JSON на базу данных?

Пока ответов немного (до тысяч) - JSON удобен. Когда записей десятки тысяч или несколько пользователей пишут одновременно, JSON-файл тормозит и возможны конфликты записи. Тогда переходите на SQLite или MongoDB.

Можно ли сделать анонимный опрос в Телеграме?

Да. У нативного send_poll есть параметр is_anonymous=True - голоса не привязываются к пользователям. В своём опроснике на aiogram достаточно не сохранять user_id и full_name, оставив только сами ответы.

Сколько ответов выдержит опрос на JSON?

Спокойно - до нескольких тысяч. Дальше JSON-файл, который читается и переписывается целиком на каждый ответ, начнёт тормозить. Если планируете популярный опрос в Телеграме с десятками тысяч прохождений, сразу закладывайте SQLite или MongoDB.

Заключение

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

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

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

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

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