Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~16 мин +20 XP

Форматирование: HTML в сообщениях

Форматирование в telegram-боте включается одним полем — msg.ParseMode = tgbotapi.ModeHTML, после чего в тексте работают теги жирного, курсива, кода и ссылок. Здесь разберём HTML-режим tgbotapi и его теги, экранирование пользовательского ввода через html.EscapeString и ошибку can't parse entities, которая валит сообщения из-за одного символа.

Содержание урока · 10 разделов

Справка бота выглядит как стена текста: /help печатает двадцать команд сплошным абзацем, и пользователи перестают её открывать — не видно, где команда, а где пояснение. Проблема не в содержании, а в подаче: Telegram умеет рисовать жирный, курсив, моноширинный код и кликабельные ссылки, но только если бот попросил об этом явно. За это отвечает поле ParseMode у конфигурации сообщения и HTML-разметка внутри текста. Цена включения — новая категория падений: сообщение с тегами проходит разбор на сервере Telegram, и одна лишняя угловая скобка от пользователя уронит его с ошибкой can't parse entities. Хорошая новость: падение локальное, оно не влияет ни на других пользователей, ни на процесс бота — Send просто вернёт ошибку, которую можно обработать. В этом уроке подключим разметку, соберём справку, которую приятно читать, и защитим всё, что бот печатает из пользовательского ввода.

ParseMode и теги HTML#

Telegram поддерживает два режима разметки — HTML и MarkdownV2, — и переключаются они строкой в поле ParseMode. В библиотеке для этого есть константа tgbotapi.ModeHTML со значением HTML: msg.ParseMode = tgbotapi.ModeHTML, и дальше весь текст сообщения разбирается как HTML-фрагмент. Набор тегов намеренно узкий: b и strong дают жирный, i и em — курсив, u подчёркивание, s зачёркивание, code — моноширинный фрагмент, pre — блок кода, a с href — ссылку, blockquote — цитату. Вложенность работает: жирный внутри ссылки, код внутри жирного. Никаких div, span и стилей — сервер примет только перечисленное, остальное либо упадёт, либо уйдёт как текст. Важно понимать механику: теги не рисует библиотека и не рисует ваш Go — они едут в Telegram как часть текста, и там уже превращаются в оформление. Поэтому и экранировать чужие угловые скобки нужно у себя, до отправки.

Справка, которую читают

GO
func helpMessage() string {
        return "<b>Команды бота-дневника</b>\n\n" +
                "/today - записи за сегодня\n" +
                "/stats - статистика по месяцам\n" +
                "/export - выгрузка в файл\n\n" +
                "Правила заполнения: <i>коротко, одним сообщением</i>\n" +
                "Пример записи: <code>бег 5 км, темп 6:10</code>\n" +
                "<a href=\"https://example.com/faq\">Подробная справка</a>"
}

msg := tgbotapi.NewMessage(chatID, helpMessage())
msg.ParseMode = tgbotapi.ModeHTML
bot.Send(msg)

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

Экранирование пользовательского ввода#

Пока текст сообщения написан вами, теги безопасны. Как только бот вставляет в разметку то, что прислал пользователь, появляется вторая сторона медали: строка с угловой скобкой ломает разбор. Классический сценарий — эхо-бот с ParseMode: пользователь пишет «люблю <3» или «2 < 3 и 5 > 4», и сервер отвечает ошибкой can't parse entities, сообщение не уходит вовсе. Ошибка приходит на конкретное сообщение: остальные отправки при этом работают, и в логах бот выглядит здоровым — падение видно только по команде конкретного пользователя. Лечение — html.EscapeString из стандартной библиотеки: функция превращает угловые скобки, амперсанд и кавычки в HTML-сущности, которые Telegram показывает как текст, а не читает как разметку. Правило разделения труда простое: свою разметку экранировать нельзя — жирный перестанет быть жирным, — а весь пользовательский ввод экранируется всегда, до склейки со строкой. Из этого следует порядок сборки: сначала экранируем ввод, потом обкладываем тегами, и никогда наоборот.

Безопасное эхо с разметкой

GO
func safeEcho(update tgbotapi.Update) string {
        name := html.EscapeString(update.Message.From.FirstName)
        text := html.EscapeString(update.Message.Text)
        return fmt.Sprintf("<b>%s</b> написал:\n%s", name, text)
}

msg := tgbotapi.NewMessage(chatID, safeEcho(update))
msg.ParseMode = tgbotapi.ModeHTML
bot.Send(msg)

Оба куска пользовательского ввода — имя и текст — проходят через html.EscapeString до попадания в шаблон. Тег b при этом остаётся живым: он написан нами после экранирования. Строка «люблю <3» дойдёт как текст, и can't parse entities не случится никогда.

HTML против MarkdownV2#

Почему в уроках курса HTML, если Markdown привычнее? Аргументы за MarkdownV2 — короткая запись: два символа обёртки вместо пары тегов. Аргументы против — экранирование: MarkdownV2 требует маскировать целую дюжину символов в пользовательском тексте — подчёркивания, звёздочки, скобки всех видов, дефисы в некоторых позициях, — и забытый символ даёт ту же can't parse entities. HTML-режим маскирует ровно три символа: скобку, амперсанд и кавычку, и делает это готовая функция стандартной библиотеки. Второй аргумент — строгость: HTML разбирается предсказуемо, а парсер Markdown в Telegram чувствителен к пробелам и переносу строк. Спорное на практике звучит так: если бот только ваш текст пишет без вставок — берите что нравится, разницы почти нет. Как только в шаблоне появляется пользовательский ввод, HTML с html.EscapeString экономит часы отладки, и я не видел обратных случаев. Переключение между режимами стоит ровно одну строку — поле ParseMode переприсваивается, — поэтому решение легко пересмотреть, если проект срастётся с вводом.

Кейс: карточка записи в боте-дневнике#

Бот-дневник из прошлых уроков подтверждает сохранённую запись карточкой: дата курсивом, сама запись как есть, счётчик серии жирным. Разметка собирается конкатенацией: date := record.Date.Format("02.01"), затем fmt.Sprintf("<i>%s</i>\n%s\n<b>Серия: %d дней</b>", date, html.EscapeString(record.Text), record.Streak). Три строки кода, а карточка выглядит живой — и в истории чата её видно с первого взгляда. Здесь работает тот же принцип, что и с callback_data из девятого урока: машинное и человеческое разведены. Хранение остаётся плоским текстом, разметка — слой отображения, и если однажды понадобится экспорт в другой канал, данные не придётся чистить от тегов. Захардкоженные в базу теги — типичный след новичка, который потом всплывает в CSV-выгрузках и уезжает на сайт вместе с браком.

Две ловушки: незакрытый тег и блок pre#

Вторая категория падений случается без всякого ввода: бот собирает сообщение кусками и теряет закрывающий тег. Открывающий b есть, закрывающий забыт — и сервер либо ругается can't parse entities, либо оформляет жирным всё до конца сообщения: вместе с подписью и контактами. Лечится это дисциплиной сборки: разметку кусков собирают функциями, у каждой пары открывающий-закрывающий тег живёт в одной строке, а сообщение собирается конкатенацией готовых кусков. Полезная привычка — тестовая отправка самому себе в личку перед релизом: один ваш chat_id как первый получатель ловит сломанную разметку раньше пользователей. Обратная крайность — двойное экранирование, когда про html.EscapeString вспоминают дважды: пользователь видит &lt;b&gt; вместо жирного. Проверка простая: экранирует функция у границы ввода, ровно один раз, а не в каждом слое.

Служебные боты часто возвращают куски логов, диффы и таблицы — и тут раскрывается тег pre: многострочный блок моноширинным с сохранением переносов, читается как терминал. Нюанс тот же, что и с обычным вводом: строки лога полны угловых скобок и амперсандов, поэтому содержимое блока тоже проходит через html.EscapeString, и только потом обкладывается pre. Формат лучше делать коротким строкам — Telegram рисует блок без переносов, и длинная строка уезжает за экран телефона. Для больших отчётов честнее документ из урока про медиа — файл не упирается в лимит текстового сообщения, а pre остаётся для коротких выдержек в диалоге.

Теги, которые понимает Telegram#

ТегЧто делаетГде уместен
b, strongжирныйзаголовки секций, акценты
i, emкурсивдаты, пояснения, ремарки
codeмоноширинный фрагменткоманды, значения, числа
preблок кода с рамкоймногострочные примеры
a hrefкликабельная ссылкасправка, внешние страницы
blockquoteцитата с отступомцитаты сообщений, отзывы

Спорное мнение: разметка не для каждого сообщения#

Освоив теги, новички обкладывают ими весь трафик бота: эхо с жирным, подтверждения курсивом, всё как в лендинге. Я считаю это ошибкой вкуса с реальной ценой: разметка уместна там, где есть структура — справка, карточки, отчёты, — и вредна в коротких диалоговых репликах, где она шумит и добавляет каждое сообщению пафос объявления. Граница рабочая: если сообщение длиннее трёх строк или содержит перечисление — разметка помогает; если это ответ «принял» или следующий вопрос анкеты — плоский текст честнее. Второй аргумент экономный: каждое сообщение с ParseMode проходит разбор на сервере, и баг разметки роняет его целиком, тогда как плоский текст сломать нечем. Третий — спокойствие пользователя: чат, в котором каждое второе сообщение оформлено как баннер, читается утомительно, и люди быстрее отключают уведомления. Разметка — приправа, а не блюдо; это правило не из документации, но из поддержки ботов следует напрямую.

Проверка: включение HTML

Как включить HTML-разметку у сообщения в tgbotapi?

Проверка: упавшее сообщение

Бот с ParseMode HTML вставил в сообщение текст пользователя «2 < 3». Что произойдёт?

Что увидит пользователь

Прочитайте код и выберите, что он выведет:

msg := tgbotapi.NewMessage(chatID,
        "<b>Привет</b>, это бот")
// ParseMode не установлен
bot.Send(msg)

Соберите разметку безопасно

Впишите поле разметки, константу режима и функцию экранирования.

msg := tgbotapi.NewMessage(chatID, text) msg. = tgbotapi. name := html.(update.Message.From.FirstName)

Шесть строк отправки с разметкой

Расставьте отправку сообщения с HTML: от конструктора до обработки ошибки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: форматирование сообщений

Термин

ParseMode

нажмите, чтобы перевернуть
Определение

Поле конфигурации сообщения, включающее разбор разметки: tgbotapi.ModeHTML или MarkdownV2. Без него теги печатаются как текст.

1 / 6

Текст бота приобрёл структуру — теперь ему нужен масштаб: урок про рассылки разошлёт разметку сотням подписчиков с паузами и обработкой ошибок, а команды и меню из седьмой части станут местом, где справка с HTML живёт постоянно. Лимиты API и вежливые паузы подробнее разбираются в уроке про ошибки и лимиты. Экранирование строк опирается на устройство строк в Go — оно есть в базовом курсе, а карта всех уроков — на странице курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Разметка включается полем ParseMode: tgbotapi.ModeHTML включает HTML-режим у сообщения.
  • Набор тегов узкий: b, i, u, s, code, pre, a, blockquote — без div и стилей.
  • Пользовательский ввод экранируется html.EscapeString до склейки с шаблоном: скобка в тексте роняет сообщение с can't parse entities.
  • Свою разметку экранировать нельзя — экранируют только вставки из ввода, и всегда до обёртки тегами.
  • HTML практичнее MarkdownV2 там, где есть пользовательский ввод: маскировать надо три символа, а не дюжину.
  • Без ParseMode теги печатаются как текст — ошибки нет, но пользователь видит скобки.
  • Разметка уместна в справках и карточках; короткие диалоговые реплики честнее оставлять плоскими.

Домашнее задание

Перепишите /help локального бота на HTML: заголовок жирным, каждая команда отдельной строкой с кодом, ссылка на страницу справки. Затем добавьте эхо с разметкой и проверьте два ввода: обычную строку и «2 < 3 и 5 > 4» — второй должен дойти как текст, а не уронить Send. Ошибку can't parse entities поймайте обратным способом: закомментируйте экранирование и посмотрите ответ API.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Форматирование: HTML в сообщениях)

Вопросы и ответы по уроку

Почему Markdown-режим в библиотеке называется MarkdownV2 и чем он отличается?

Старый режим Markdown в Bot API объявлен устаревшим: актуальный называется MarkdownV2 и строже в экранировании. Библиотека держит константу ModeMarkdownV2, но для ботов с пользовательским вводом HTML обычно удобнее.

Можно ли комбинировать разметку с кнопками?

Да: ReplyMarkup и ParseMode независимы, сообщение с inline-клавиатурой спокойно несёт жирные заголовки и ссылки. Это стандартная пара для карточек с кнопками действий.

Какой длины может быть подпись с разметкой?

Лимит считается по итоговой строке: 4096 символов на текстовое сообщение, 1024 на подпись медиа — после разбора разметки. Экранирование раздувает строку, поэтому на грани лимита оставляйте запас.

Термины урока

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

Следующая ступень учебного пути

Prometheus25 уроков~12 чпрактика25 уроков уже доступны

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99