Сегодня мы разберем как сделать опросник в 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/