Эхо-бот и работа с текстом

Учимся доставать текст из сообщения, повторять его и переделывать: кричать капсом, переворачивать задом наперёд и считать слова — всё это твой «Цыплёнок-помощник» сделает прямо в чате.
Главная мысль урока: любое сообщение, которое пользователь шлёт боту, приходит к тебе обычной строкой Python в message.text — а со строками ты уже умеешь делать что угодно.

Зачем это вообще нужно

Представь: ты сидишь в чате игрового клана, и кто-то пишет сообщение шёпотом, мелкими буквами, а ты хочешь, чтобы оно прозвучало громко, КАПСОМ, как боевой клич. Или другу нужно быстро узнать, сколько слов он наболтал в своём эссе по литературе, чтобы не превысить лимит. Или просто хочется прикольнуться и отправить текст задом наперёд, чтобы получился секретный шифр.

Всё это умеет делать бот, который сначала читает твоё сообщение, а потом переделывает его и отправляет обратно. Самый простой такой бот — это эхо-бот. Ты пишешь ему «привет», он отвечает «привет». Звучит как игрушка, но именно с эхо-бота начинается вся серьёзная работа с текстом: ведь чтобы что-то переделать, надо сначала научиться это что-то взять в руки.

Вот к чему мы придём в конце урока — бот, который не просто повторяет, а ещё и кричит твоё сообщение капсом:

Ты: привет, цыплёнок
Цыплёнок-помощник: ПРИВЕТ, ЦЫПЛЁНОК

В прошлом уроке про хэндлеры сообщений ты научился ловить любое текстовое сообщение и реагировать на него. Сегодня мы пойдём дальше: научимся доставать содержимое этого сообщения и работать с ним как с обычной строкой.

Откуда у бота берётся текст: объект message

Когда кто-то пишет твоему боту, Telegram упаковывает это сообщение в коробку с кучей информации: кто отправил, когда, в каком чате, есть ли картинка, и — самое для нас важное — какой там текст. Эту коробку aiogram передаёт в твой хэндлер под именем message.

Думай про message как про посылку с почты. На посылке есть наклейки: от кого (message.from_user), в какой чат (message.chat), а внутри лежит сам текст письма — это message.text. Чтобы прочитать письмо, ты не разрываешь всю коробку, а просто достаёшь нужную наклейку: message.text и есть та самая строка, которую напечатал пользователь.

Запомни: message.text — это обычная строка Python (тип str). Всё, что ты знаешь про строки — .upper(), .split(), срезы [::-1] — работает с ней без всякой магии.

Маленький, но важный нюанс: если пользователь прислал не текст, а стикер, фото или голосовое, то message.text будет равен None — то есть «пусто». Про это мы поговорим в разделе про ошибки, потому что именно тут новички спотыкаются чаще всего.

Почему вообще текст приходит строкой, а не как-то иначе? Потому что под капотом Telegram общается с твоей программой через Bot API — это набор HTTP-запросов, в которых данные передаются текстом в формате JSON. Когда приходит новое сообщение, Telegram присылает целое обновление (update) — пакет данных о том, что произошло. aiogram разбирает этот пакет за тебя и собирает удобный объект message, из которого ты достаёшь поле text в одну строчку. Тебе не нужно вручную парсить JSON или думать про HTTP — библиотека делает всю грязную работу, а тебе остаётся работать с обычной строкой. Это и есть главная причина, почему мы используем aiogram, а не дёргаем Bot API напрямую: меньше рутины, больше времени на интересную логику.

Пример 1. Самый простой эхо-бот

Начнём с классики. Наш «Цыплёнок-помощник» уже умеет отвечать на /start из прошлых уроков. Теперь добавим к нему хэндлер, который ловит любое текстовое сообщение и отправляет его обратно слово в слово. Мы продолжаем работать в том же файле bot.py с теми же объектами bot и dp.

from aiogram import F
from aiogram.types import Message

# bot и dp уже созданы выше, как в прошлых уроках

@dp.message(F.text)
async def echo_handler(message: Message):
    text = message.text          # достаём строку из сообщения
    await message.answer(text)   # отправляем её обратно

Результат: в чате бот ответит ровно тем же текстом, который ты ему написал. Напишешь «эй, цыплёнок» — получишь «эй, цыплёнок».

Разберём по строчкам, что тут происходит:

  • @dp.message(F.text) — это декоратор. Он говорит диспетчеру: «вызывай эту функцию, когда придёт сообщение, у которого есть текст». Кусочек F.text — это фильтр, такой проверочный пропуск: внутрь пускаем только сообщения с текстом, а стикеры и фото отсеиваем сразу. Так мы заранее страхуемся от пустого message.text.
  • async def echo_handler(message: Message) — сама функция-хэндлер. Слово async означает, что функция асинхронная, но тебе пока достаточно помнить правило: внутри неё перед вызовами бота ставим await. Бот как официант — пока он несёт ответ на «кухню» Telegram, программа не зависает, а может обслуживать других.
  • text = message.text — достаём строку в отдельную переменную, чтобы с ней было удобно работать дальше.
  • await message.answer(text) — отправляем строку обратно в тот же чат. Метод answer — это удобный способ ответить туда, откуда пришло сообщение, не указывая chat_id вручную. Про разные способы отправки текста мы подробно говорили в уроке про отправку и форматирование текста.

Готово — это уже рабочий эхо-бот. Но повторять одно и то же скучно. Давай научим Цыплёнка менять текст.

Пример 2. Бот, который кричит капсом

У строк в Python есть метод .upper() — он возвращает новую строку, где все буквы стали заглавными. Прежде чем встраивать его в бота, проверим, как он работает, на маленьком чистом сниппете — его можно запустить прямо в браузере, потому что он использует только стандартный Python без всякого aiogram.

text = "тише, идёт урок"
print(text.upper())

Вывод:

ТИШЕ, ИДЁТ УРОК

Видишь? .upper() аккуратно поднял регистр даже у русских букв, включая «ё». Теперь встроим это в нашего бота — заменим тело хэндлера всего одной строкой:

@dp.message(F.text)
async def shout_handler(message: Message):
    loud = message.text.upper()   # делаем текст громким
    await message.answer(loud)

Результат: в чате бот ответит твоим же сообщением, но капсом. Напишешь «привет, цыплёнок» — получишь «ПРИВЕТ, ЦЫПЛЁНОК», как в обещанном вначале примере.

Обрати внимание: мы вызвали .upper() прямо у message.text, не сохраняя промежуточную переменную. Это нормально — message.text ведёт себя как любая строка, и методы можно навешивать на него цепочкой. Главное правило: строковые методы не меняют исходную строку, а возвращают новую. Поэтому результат обязательно надо куда-то положить или сразу передать в answer.

Пример 3. Переворачиваем строку — секретный шифр

А теперь немного магии для друзей. Сделаем бота, который переворачивает текст задом наперёд. В Python это делается срезом [::-1] — он читает строку с конца к началу. Снова проверим на чистом сниппете:

text = "цыплёнок"
print(text[::-1])

Вывод:

конёлпыц

Срез [::-1] — это «возьми всю строку, но иди шагом минус один», то есть справа налево. Встроим в бота:

@dp.message(F.text)
async def reverse_handler(message: Message):
    reversed_text = message.text[::-1]
    await message.answer(reversed_text)

Результат: в чате бот ответит твоим сообщением, написанным наоборот. Напишешь «секрет» — получишь «теркес». Покажи другу — пусть гадает, как ты это зашифровал.

Почему это работает именно так? Срез — это способ взять из строки кусок: в квадратных скобках через двоеточие записывают «откуда», «докуда» и «с каким шагом». Когда первые два места пустые, Python берёт строку целиком, а третье число — это шаг. Шаг 1 означает «иди по одному символу слева направо», а шаг -1 — «иди по одному символу, но в обратную сторону». Поэтому [::-1] и читается как «вся строка задом наперёд». Этот же приём работает не только со строками, но и со списками — так что ты только что выучил универсальный трюк Python, а не какую-то особенность ботов.

Пример 4. Считаем слова в сообщении

Последний приём на сегодня — самый полезный для учёбы. Сделаем так, чтобы бот считал, сколько слов прислал пользователь. Это спасает, когда нужно уложиться в лимит по сочинению или проверить длину поста.

Чтобы посчитать слова, мы сначала разрежем строку по пробелам методом .split(). Он возвращает список слов. А длину списка узнаём функцией len(). Проверим логику на чистом сниппете:

text = "бот считает мои слова"
words = text.split()
print(words)
print(len(words))

Вывод:

['бот', 'считает', 'мои', 'слова']
4

Метод .split() без аргументов умный: он сам схлопывает лишние пробелы и не создаёт пустых слов, даже если ты поставил два пробела подряд. Теперь соберём ответ бота. Чтобы вставить число в текст, используем f-строку — это строка с буквой f перед кавычками, в которую можно подставлять значения через фигурные скобки:

@dp.message(F.text)
async def count_handler(message: Message):
    words = message.text.split()
    count = len(words)
    await message.answer(f"В твоём сообщении {count} слов(а).")

Результат: в чате бот посчитает слова. Напишешь «бот считает мои слова» — получишь «В твоём сообщении 4 слов(а).».

Собираем всё вместе: бот-меню преобразований

В реальном боте обычно не хочется выбирать заранее, что он будет делать. Давай объединим приёмы: пусть Цыплёнок на одно сообщение выдаёт сразу все три превращения. Так ты увидишь, как несколько строковых операций уживаются в одном хэндлере.

@dp.message(F.text)
async def transform_handler(message: Message):
    text = message.text
    loud = text.upper()
    reversed_text = text[::-1]
    count = len(text.split())
    answer = (
        f"Капсом: {loud}\n"
        f"Наоборот: {reversed_text}\n"
        f"Слов: {count}"
    )
    await message.answer(answer)

Результат: в чате на сообщение «привет цыплёнок» бот ответит тремя строками — «Капсом: ПРИВЕТ ЦЫПЛЁНОК», «Наоборот: конёлпыц тевирп» и «Слов: 2». Заметь: символ \n внутри строки означает перенос строки, поэтому ответ красиво разбивается на три строчки.

Частые ошибки и подводные камни

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

1. message.text равен None — и всё падает

Если у тебя в хэндлере не стоит фильтр F.text, бот будет ловить вообще все сообщения, включая стикеры и фото. А у стикера message.text — это None, и попытка вызвать None.upper() приводит к ошибке AttributeError. Лечение простое: всегда ставь фильтр @dp.message(F.text), как мы делали во всех примерах. Тогда внутрь хэндлера попадут только сообщения с текстом.

2. Строковые методы ничего не меняют «на месте»

Очень частая ошибка новичка — написать так:

text = message.text
text.upper()              # результат потерян!
await message.answer(text)   # отправит исходный текст, не капс

Здесь text.upper() создаёт новую строку, но её никуда не сохранили, поэтому она тут же исчезает. Бот отправит исходный текст без изменений. Правильно — text = text.upper() или сразу передать результат в answer.

3. Путаница: answer и send_message

Метод message.answer(text) отвечает в тот чат, откуда пришло сообщение — это самый удобный способ. Иногда новички пытаются вызвать message.send_message(...) — такого метода нет, и будет ошибка. Метод send_message есть у объекта bot, и ему нужно явно передавать chat_id. Для ответа в тот же чат используй message.answer.

4. Забыл await

Если написать message.answer(text) без await, бот промолчит, а в консоли появится предупреждение вроде «coroutine was never awaited». Запомни: любой вызов, который шлёт что-то в Telegram, должен идти с await. Это как сказать официанту «отнеси заказ» и дождаться, что он его действительно отнёс.

5. Лишние пробелы и пустые строки

Если хочешь убрать пробелы по краям сообщения (например, пользователь случайно нажал пробел в начале), пригодится метод .strip(): message.text.strip() вернёт строку без пробелов по краям. Для подсчёта слов .split() и так справляется с пробелами, но при сравнении текста .strip() часто спасает от неожиданных багов.

Мини-практика: бот-зеркало с командой

Теперь твоя очередь. Доработай «Цыплёнка-помощника» так, чтобы он стал умным зеркалом. Вот задание:

  1. Сделай хэндлер на текстовые сообщения, который убирает пробелы по краям (.strip()) и отправляет назад текст, где первая буква каждого слова заглавная. Подсказка: у строк есть готовый метод .title() — проверь его на чистом сниппете с print(), прежде чем встраивать в бота.
  2. Добавь подсчёт символов в сообщении (не слов, а букв и пробелов) — это просто len(message.text). Пусть бот в конце ответа дописывает «Символов: N».
  3. Подумай, что произойдёт, если прислать боту пустое сообщение из одних пробелов. Защитись от этого: если после .strip() строка пустая, пусть бот ответит «Ты прислал пустое сообщение, цыплёнок не понял».

Если получится — у тебя в руках настоящий мини-инструмент обработки текста, который не стыдно показать друзьям. А если застрянешь — вернись к примерам 2 и 4, там есть все нужные кусочки.

Итоги и что дальше

Сегодня ты сделал большой шаг: твой бот перестал быть «попугаем» и научился понимать и переделывать текст. Давай закрепим главное:

  • Текст сообщения лежит в message.text — это обычная строка Python.
  • Фильтр F.text страхует от None, пропуская только сообщения с текстом.
  • Со строкой работают все привычные приёмы: .upper() для капса, срез [::-1] для переворота, .split() и len() для подсчёта слов.
  • Строковые методы возвращают новую строку — результат надо сохранять.
  • Отправляем ответ через await message.answer(...), не забывая про await.

В следующих уроках мы научим Цыплёнка не просто отвечать текстом, а показывать кнопки и меню, чтобы пользователю не приходилось всё печатать руками. А ещё дальше — вести с человеком настоящий диалог по шагам через FSM, как анкета, где поля заполняются по очереди. Но фундамент ты уже заложил: умение читать и преобразовывать текст пригодится в каждом из этих уроков. Молодец, что дошёл до конца — двигаемся дальше!

Проверьте себя
1. Где в хэндлере лежит текст, который прислал пользователь?
Amessage.text
Bmessage.content
Cmessage.value
Dbot.text
2. Что вернёт выражение message.text.upper(), если пользователь прислал «привет»?
Aпривет
BПРИВЕТ
CПривет
Dтевирп
3. Зачем в хэндлере ставят фильтр F.text?
AЧтобы бот отвечал быстрее
BЧтобы пропускать только сообщения с текстом и не получить None у стикеров и фото
CЧтобы текст автоматически переводился на английский
DЧтобы бот игнорировал команду /start
4. Почему этот код отправит исходный текст без изменений: text = message.text; text.upper(); await message.answer(text)?
AПотому что .upper() работает только с английскими буквами
BПотому что .upper() меняет строку на месте, но слишком медленно
CПотому что .upper() возвращает новую строку, а результат никуда не сохранили
DПотому что забыли поставить await перед .upper()
5. Как из строки в переменной text получить количество слов?
Alen(text)
Blen(text.split())
Ctext.count()
Dtext.words()
6. Что делает срез text[::-1]?
AУдаляет первый символ строки
BВозвращает строку, прочитанную задом наперёд
CПереводит строку в верхний регистр
DОставляет только каждый второй символ