Отправка текста и форматирование

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

Форматирование — это разметка внутри текста сообщения, которая говорит Telegram: «вот это слово сделай жирным, а это — ссылкой». Сам текст ты пишешь как обычно, а оформление добавляешь специальными тегами.

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

Зачем вообще форматировать текст

Представь, что ты пишешь другу в личку важное расписание: «завтра матч в 18:00 не опаздывай форма синяя». Каша, да? А теперь представь то же самое, но время — жирным, «не опаздывай» — тоже жирным, а «форма синяя» — отдельной строкой. Сразу понятно, что главное, а что нет.

С ботом ровно так же. Когда «Цыплёнок-помощник» отвечает тебе про погоду или про домашку, ему нужно выделять важное: температуру, дедлайн, название предмета. Иначе пользователь просто пробежит глазами и ничего не запомнит.

Вот к чему мы придём к концу урока — бот будет отвечать так:

@chick_helper_bot
Привет! Я Цыплёнок-помощник 🐤

Сегодня по плану:
• Математика — стр. 45, № 12
• История — параграф 7

Подробности: открой дневник

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

Главный инструмент: message.answer

Когда боту приходит сообщение, aiogram передаёт его в хэндлер в виде объекта message. У этого объекта есть метод answer — он отправляет ответ в тот же чат, откуда пришло сообщение. Думай о нём как о кнопке «Ответить» в мессенджере: тебе не надо помнить, кому и куда писать, метод сам отправит туда, откуда пришёл вопрос.

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.types import Message

@dp.message(CommandStart())
async def start_handler(message: Message):
    await message.answer("Привет! Я Цыплёнок-помощник 🐤")

Результат: в чате бот ответит обычным текстом «Привет! Я Цыплёнок-помощник 🐤» без какого-либо оформления.

Обрати внимание на await перед message.answer. Отправка сообщения — это поход к серверам Telegram, и он занимает время. Слово await говорит: «подожди, пока ответ дойдёт, и только потом иди дальше». Без него бот попытается отправить сообщение и тут же забудет об этом — текст до пользователя не долетит. Это та самая асинхронность, которой не надо бояться: просто ставь await перед всем, что общается с Telegram.

Разметка: как сказать «сделай жирным»

Сам по себе message.answer ничего не форматирует — он шлёт текст как есть. Чтобы Telegram понял разметку, нужно две вещи: расставить специальные теги в тексте и сказать боту, на каком «языке разметки» этот текст написан. Этот язык задаётся параметром parse_mode.

Есть два таких языка: HTML и MarkdownV2. Они делают одно и то же, просто пишутся по-разному. Мы будем использовать HTML — он почти такой же, как теги на сайтах, и его проще читать.

HTML-разметка по полочкам

Принцип простой: оборачиваешь кусок текста в парный тег. Открывающий тег — <b>, закрывающий — </b> со слешем. Всё, что между ними, оформится.

Что хотимТегПример
Жирный<b>…</b><b>важно</b>
Курсив<i>…</i><i>тихо</i>
Зачёркнутый<s>…</s><s>отменено</s>
Моноширинный (код)<code>…</code><code>№ 12</code>
Ссылка<a href="…">…</a><a href="https://t.me">тут</a>

Теперь подключим это к боту. Задать parse_mode можно один раз для всего бота — тогда не придётся повторять его в каждом сообщении.

from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.types import Message

bot = Bot(
    token=TOKEN,
    default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
dp = Dispatcher()

@dp.message()
async def schedule_handler(message: Message):
    await message.answer(
        "Я <b>Цыплёнок-помощник</b> 🐤\n"
        "Сегодня: <i>математика</i> и <i>история</i>\n"
        "Задание: <code>№ 12"
    )

Результат: в чате бот ответит сообщением, где «Цыплёнок-помощник» будет жирным, слова «математика» и «история» — курсивом, а «№ 12» — моноширинным шрифтом, будто кусочек кода.

Здесь DefaultBotProperties(parse_mode=ParseMode.HTML) — это как настройка по умолчанию для объекта bot: «весь текст, который я отправляю, читай как HTML». Один раз настроил — и забыл. Если же тебе нужно в одном конкретном сообщении отключить разметку, можно передать parse_mode=None прямо в message.answer.

Многострочные сообщения и переносы

Чтобы сообщение шло несколькими строками, используется \n — это символ перевода строки. Каждый \n — новая строка в чате. А ещё подряд идущие строки в Python склеиваются, если просто поставить их рядом в скобках, как в примере выше: "...\n" и следующая строка автоматически приклеятся друг к другу. Это удобно, когда сообщение длинное.

Ссылки: спрятать длинный URL за словом

Голый адрес https://t.me/durov в чате выглядит громоздко. С тегом <a> можно спрятать его за обычным словом.

@dp.message()
async def link_handler(message: Message):
    await message.answer(
        "Открой свой <a href=\"https://dnevnik.ru\">дневник</a>, "
        "чтобы увидеть оценки."
    )

Результат: в чате бот ответит «Открой свой дневник, чтобы увидеть оценки», где слово «дневник» будет синей кликабельной ссылкой, ведущей на dnevnik.ru.

Экранирование: когда символы ломают разметку

Вот где новички спотыкаются чаще всего. Раз ты сказал боту «читай текст как HTML», то символы <, > и & Telegram воспринимает как часть разметки, а не как обычные буквы. Если пользователь спросит у бота «сколько будет 5 < 7?», и ты вставишь этот текст прямо в сообщение, Telegram решит, что < 7? — это начало какого-то тега, запутается и вернёт ошибку.

Решение — экранирование: заменить опасные символы на их безопасные «псевдонимы». Telegram понимает три замены:

  • < заменяем на &lt;
  • > заменяем на &gt;
  • & заменяем на &amp;

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

def escape_html(text):
    text = text.replace("&", "&")
    text = text.replace("<", "<")
    text = text.replace(">", ">")
    return text

user_text = "5 < 7 & это правда"
print(escape_html(user_text))

Вывод:

5 &lt; 7 &amp; это правда

Заметь важную деталь: & заменяем первым. Если сделать наоборот, то после замены < на &lt; мы получим амперсанд, который потом сами же испортим. Порядок важен.

В реальном боте не надо писать эту функцию руками — aiogram уже умеет это. Импортируешь хелпер и оборачиваешь им любой текст от пользователя:

from aiogram.utils.markdown import html_decoration as hd
from aiogram.types import Message

@dp.message()
async def math_handler(message: Message):
    safe = hd.quote(message.text)
    await message.answer(f"Ты написал: <b>{safe}</b>")

Результат: в чате бот безопасно повторит твоё сообщение жирным шрифтом, даже если в нём были символы <, > или & — они отобразятся как обычные знаки, а не сломают сообщение.

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

Через эти грабли проходят почти все. Давай пройдёмся, чтобы ты их обошёл.

1. Забыл parse_mode — теги видны как текст

Ты написал <b>Привет</b>, а в чате так и появилось «<b>Привет</b>» со скобками. Значит, ты не сказал боту, что текст — это HTML. Проверь, что задал parse_mode=ParseMode.HTML в настройках бота или в самом message.answer.

2. Незакрытый тег — ошибка от Telegram

Открыл <b>, а закрыть </b> забыл — и бот молчит, а в консоли красная ошибка вроде «can't parse entities». Telegram строго требует, чтобы каждый открывающий тег имел свою пару. Считай теги как скобки в коде: сколько открыл, столько закрой.

3. Вставил текст пользователя без экранирования

Самая коварная. Бот работает месяцами, а потом кто-то присылает сообщение с символом <, и бот падает с ошибкой. Правило простое: любой текст, пришедший от пользователя (его имя, его сообщение), перед вставкой в HTML-разметку прогоняй через hd.quote(...). Свой собственный текст экранировать не надо.

4. Спутал HTML и MarkdownV2

Если ты выбрал ParseMode.HTML, то звёздочки *жирный* работать не будут — это синтаксис Markdown. И наоборот. Выбери один язык разметки и держись его во всём боте, чтобы не путаться.

5. Сообщение длиннее 4096 символов

Telegram не пропустит сообщение длиннее 4096 символов — вернёт ошибку. Если бот шлёт что-то огромное (например, длинный список), разбивай его на несколько message.answer или сокращай.

Мини-практика: бот-эхо с подсветкой

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

  1. Первая строка — жирным: «Ты написал:».
  2. Вторая строка — само сообщение пользователя, но моноширинным шрифтом (тег <code>), и обязательно экранированное через hd.quote.
  3. Третья строка — курсивом: сколько в сообщении символов (подсказка: len(message.text)).

Каркас, который надо дополнить:

from aiogram.utils.markdown import html_decoration as hd
from aiogram.types import Message

@dp.message()
async def echo_handler(message: Message):
    safe = hd.quote(message.text)
    length = len(message.text)
    # допиши message.answer с тремя строками:
    # жирный заголовок, моноширинный текст, курсивный счётчик
    await message.answer(...)

Результат: когда ты пришлёшь боту «привет <3», он ответит карточкой: жирное «Ты написал:», на новой строке моноширинное «привет <3» (символы не сломают сообщение), и курсивом «Символов: 9».

Если справился — поздравляю, ты уже умеешь оформлять сообщения лучше, чем половина учебных ботов в интернете.

Итоги

Сегодня «Цыплёнок-помощник» научился говорить красиво. Коротко, что ты теперь знаешь:

  • message.answer отправляет ответ в тот же чат — не забывай await перед ним.
  • parse_mode=ParseMode.HTML в настройках бота включает HTML-разметку для всех сообщений.
  • Теги <b>, <i>, <code>, <a> делают текст жирным, курсивным, моноширинным и ссылкой.
  • Текст от пользователя обязательно экранируй через hd.quote(...), иначе символы < > & сломают сообщение.

В следующем уроке мы дадим Цыплёнку настоящие кнопки — те самые, что появляются под сообщением и под полем ввода. Текст — это здорово, но когда пользователю не надо ничего печатать, а можно просто нажать «Погода» или «Домашка», бот сразу становится в разы удобнее. До встречи!

Проверьте себя
1. Что делает await перед message.answer(...)?
AДелает текст сообщения жирным
BЗаставляет программу дождаться, пока сообщение действительно уйдёт в Telegram
CЭкранирует опасные символы в тексте
DУдаляет сообщение через несколько секунд
2. Какой параметр нужно задать, чтобы Telegram воспринимал теги <b> и <i> как разметку, а не как обычный текст?
Aformat_text
Bparse_mode
Creply_markup
Ddisable_web_page_preview
3. Каким тегом сделать слово моноширинным, как кусочек кода?
A<i>…</i>
B<b>…</b>
C<code>…</code>
D<mono>…</mono>
4. Зачем экранировать текст, пришедший от пользователя, перед вставкой в HTML-сообщение?
AЧтобы сообщение отправлялось быстрее
BЧтобы символы <, > и & не сломали разметку и не вызвали ошибку Telegram
CЧтобы текст автоматически стал жирным
DЧтобы скрыть текст от других пользователей
5. В каком порядке надо выполнять замены при ручном экранировании HTML?
AСначала < и >, потом &
BСначала &, потом < и >
CПорядок не важен
DСначала >, потом < и &
6. Бот отправил сообщение, и в чате видно «<b>Привет</b>» прямо со скобками. В чём причина?
AНе задан parse_mode (Telegram не знает, что это HTML)
BТекст слишком длинный
CЗабыли await
DИспользован запрещённый тег