Новостной бот: парсинг и отправка
Учим «Цыплёнка-помощника» приносить свежие новости: бот ходит за лентой во внешний источник, выдёргивает из ответа заголовки и ссылки и присылает тебе аккуратную подборку из нескольких пунктов.
Парсинг — это разбор «сырого» ответа от сервера на понятные кусочки: из большой кучи данных ты достаёшь именно то, что тебе нужно — заголовок новости и ссылку на неё, — и выбрасываешь остальное.
В прошлом уроке про запросы к API через aiohttp мы научили «Цыплёнка-помощника» ходить в интернет и приносить ответ от чужого сервера — например, погоду. Но там ответ был коротеньким: пара чисел, и всё. Сегодня задача интереснее: лента новостей — это не одно число, а целый список из десятков статей, и из каждой надо аккуратно вытащить заголовок и ссылку. Это и есть парсинг, и сейчас ты увидишь, что ничего страшного в нём нет.
Зачем боту приносить новости
Представь: у тебя есть Telegram-бот для друзей-геймеров. Каждое утро кто-то спрашивает в чате «чё там нового вышло?», и приходится лезть на сайты, копировать ссылки, кидать их вручную. Скучно. А что если бот сам сходит за свежими новостями про игры и пришлёт подборку из пяти заголовков, на каждый из которых можно нажать и сразу открыть статью?
Именно к этому мы придём к концу урока. Вот как будет выглядеть результат команды /news в чате:
@chick_helper_bot
🐤 Свежие новости для тебя:
1. Вышло крупное обновление для популярной игры
https://example-news.ru/big-update
2. Анонсирован турнир с призовым фондом
https://example-news.ru/tournament-2026
3. Разработчики показали трейлер новой части
https://example-news.ru/new-trailer
Результат: по команде /news бот сходит за лентой новостей, возьмёт из неё несколько первых статей и пришлёт пронумерованный список «заголовок + ссылка». На каждую ссылку можно нажать прямо в Telegram и открыть новость.
По дороге мы разберём две вещи: откуда брать саму ленту (через API или через RSS) и как из полученного ответа достать нужные кусочки. Это и есть «парсинг и отправка» из названия урока. Звучит как два сложных слова, а на деле всё та же знакомая цепочка: бот ходит в интернет, получает текст и аккуратно разбирает его на части. Просто в этот раз частей много, и их надо собрать в красивый список.
Откуда берётся лента: API и RSS
Чтобы прислать новости, бот должен сначала их откуда-то получить. Источников два, и оба работают по уже знакомой схеме: бот делает HTTP-запрос и получает в ответ текст. Разница — в формате этого текста.
Вариант 1: новостной API (JSON)
Многие новостные сервисы дают свой API — адрес, по которому, если зайти, тебе вернут новости в виде JSON. JSON мы уже видели в уроке про погоду: это вложенные словари и списки, прямо как в Python. Выглядит ответ примерно так:
{
"articles": [
{
"title": "Вышло крупное обновление для популярной игры",
"url": "https://example-news.ru/big-update"
},
{
"title": "Анонсирован турнир с призовым фондом",
"url": "https://example-news.ru/tournament-2026"
}
]
}
Результат: сервер вернул словарь, внутри которого по ключу articles лежит список статей. Каждая статья — это маленький словарь с ключами title (заголовок) и url (ссылка).
Это самый удобный для нас формат: данные уже разложены по полочкам, бери да доставай по ключам. Минус один — у многих API надо регистрироваться и получать свой ключ (отдельный, не путай с токеном бота!). Этот ключ, как и токен, секретный, и его тоже держат в переменных окружения, а не в коде. Зато взамен ты получаешь чистые, заранее причёсанные данные: сервис уже сам отобрал свежие статьи и сложил их в аккуратный список, тебе остаётся только пробежаться по нему циклом.
Вариант 2: RSS-лента (XML)
Почти у каждого сайта с новостями есть RSS — специальный адрес (часто вида /rss или /feed), по которому сайт отдаёт свои свежие материалы в стандартном формате XML. Регистрироваться обычно не нужно — просто заходи и читай. Это как «дайджест от сайта» в машиночитаемом виде.
XML похож на HTML: данные обёрнуты в теги вида <тег>значение</тег>. Кусочек RSS-ленты выглядит так:
<rss>
<channel>
<item>
<title>Разработчики показали трейлер новой части</title>
<link>https://example-news.ru/new-trailer</link>
</item>
<item>
<title>Анонсирован турнир с призовым фондом</title>
<link>https://example-news.ru/tournament-2026</link>
</item>
</channel>
</rss>
Результат: сервер вернул XML, в котором каждая новость — это блок <item>, а внутри него заголовок лежит в теге <title>, ссылка — в теге <link>.
Какой вариант выбрать? Если у выбранного сервиса есть удобный JSON-API — бери его, с ним меньше возни. Если нет, а RSS есть почти у всех — берём RSS. В уроке мы разберём оба, но в финальном боте остановимся на RSS: он бесплатный и не требует ключа.
Парсинг: достаём заголовки и ссылки
Получить ответ — полдела. Теперь надо распарсить его: пройтись по списку статей и из каждой вытащить заголовок и ссылку. Разберём оба формата на маленьких самодостаточных примерах — их можно запустить прямо в браузере, потому что это чистый Python без всякого Telegram.
Парсим JSON
JSON в Python превращается в обычные словари и списки. Достаём список статей по ключу и идём по нему циклом:
data = {
"articles": [
{"title": "Вышло крупное обновление", "url": "https://news.ru/a1"},
{"title": "Анонсирован турнир", "url": "https://news.ru/a2"},
{"title": "Показали новый трейлер", "url": "https://news.ru/a3"},
]
}
articles = data["articles"]
for i, item in enumerate(articles[:2], start=1):
print(f"{i}. {item['title']}")
print(item["url"])
Вывод:
1. Вышло крупное обновление https://news.ru/a1 2. Анонсирован турнир https://news.ru/a2
Что здесь происходит по шагам:
data["articles"]— достаём из словаря список статей по ключуarticles.articles[:2]— берём срез: только первые две статьи. В реальном боте поставишь, скажем,[:5], чтобы не заваливать человека двадцатью ссылками.enumerate(..., start=1)— нумерует элементы начиная с единицы, чтобы получился список1.,2.,3.item['title']иitem['url']— у каждой статьи достаём заголовок и ссылку по их ключам.
Парсим XML (RSS)
С XML чуть хитрее: его надо сначала разобрать в дерево тегов. В стандартной библиотеке Python для этого есть модуль xml.etree.ElementTree — ничего ставить не нужно. Он превращает текст XML в дерево, по которому можно искать нужные теги:
import xml.etree.ElementTree as ET
xml_text = """
<rss>
<channel>
<item>
<title>Показали новый трейлер</title>
<link>https://news.ru/trailer</link>
</item>
<item>
<title>Анонсирован турнир</title>
<link>https://news.ru/cup</link>
</item>
</channel>
</rss>
"""
root = ET.fromstring(xml_text)
for i, item in enumerate(root.iter("item"), start=1):
title = item.find("title").text
link = item.find("link").text
print(f"{i}. {title}")
print(link)
Вывод:
1. Показали новый трейлер https://news.ru/trailer 2. Анонсирован турнир https://news.ru/cup
Разберём новые слова:
ET.fromstring(xml_text)— берёт текст XML и строит из него дерево;root— это его корень (тег<rss>).root.iter("item")— проходит по всем тегам<item>в дереве, где бы они ни лежали. Каждыйitem— одна новость.item.find("title")— ищет внутри новости тег<title>, а.textдостаёт его содержимое (сам заголовок без тегов). Так же берём и<link>.
Видишь? И JSON, и XML в итоге сводятся к одному и тому же: пройтись циклом по статьям и из каждой достать две строки. Парсинг — это просто «разбор по нужным кусочкам», и пугаться тут нечего.
Ходим за лентой через aiohttp
Маленькие примеры выше работали на готовых данных. В настоящем боте ленту надо сначала скачать — а это мы уже умеем делать через aiohttp из прошлого урока. Напомню схему: открываем сессию, делаем GET-запрос, забираем ответ. Только теперь нам нужен не .json(), а .text() — потому что RSS приходит текстом:
import aiohttp
import xml.etree.ElementTree as ET
RSS_URL = "https://example-news.ru/rss"
async def fetch_news(limit: int = 5):
async with aiohttp.ClientSession() as session:
async with session.get(RSS_URL) as response:
xml_text = await response.text()
root = ET.fromstring(xml_text)
news = []
for item in root.iter("item"):
title = item.find("title").text
link = item.find("link").text
news.append((title, link))
if len(news) == limit:
break
return news
Результат: функция fetch_news() сходит по адресу RSS-ленты, скачает XML, разберёт его и вернёт список из limit новостей — каждая в виде пары (заголовок, ссылка). Например, [("Вышло обновление", "https://..."), ("Анонсирован турнир", "https://...")].
Что важно заметить:
- Функция
async, потому что внутри она ждёт ответа из сети (await). Это та самая асинхронность: пока бот ждёт новости с чужого сервера, он не «зависает», а может отвечать другим пользователям. - Мы не тащим всю ленту целиком: как только набрали
limitновостей, делаемbreakи выходим из цикла. Лишнее нам не нужно. - Функция возвращает чистые данные — список пар, без всякого Telegram. Отправкой займётся хэндлер. Так удобнее: «добыть» и «показать» — разные задачи.
Собираем подборку и отправляем пользователю
Лента добыта и разобрана — осталось красиво её оформить и прислать. Сначала превратим список пар в один аккуратный текст, потом отправим его командой /news. Вот кусок bot.py, который добавляется к нашему «Цыплёнку-помощнику»:
from aiogram.filters import Command
from aiogram.types import Message
def format_news(news: list) -> str:
if not news:
return "🐤 Пока не нашёл свежих новостей, загляни попозже!"
lines = ["🐤 Свежие новости для тебя:\n"]
for i, (title, link) in enumerate(news, start=1):
lines.append(f"{i}. {title}\n{link}\n")
return "\n".join(lines)
@dp.message(Command("news"))
async def news_handler(message: Message):
await message.answer("🐤 Минутку, бегу за новостями...")
try:
news = await fetch_news(limit=5)
except Exception:
await message.answer("🐤 Не получилось достать новости. Попробуй ещё раз чуть позже.")
return
await message.answer(format_news(news), disable_web_page_preview=True)
Результат: по команде /news бот сначала пишет «Минутку, бегу за новостями...», затем скачивает и разбирает ленту и присылает пронумерованную подборку из пяти заголовков со ссылками. Если что-то пошло не так (сайт недоступен, лента сломалась) — бот вежливо извинится, а не упадёт.
Разберём по шагам:
format_news()— чистая функция, которая собирает из списка пар один текст. Сначала проверяет, не пустой ли список (вдруг новостей не нашлось), потом строит строки вида «номер. заголовок» и под ним ссылку."\n".join(lines)— склеивает все строчки в одно сообщение через переносы строк. Собирать длинный текст списком и склеивать в конце — привычка хорошего тона, аккуратнее, чем плюсовать строки по одной.- Перед запросом бот отвечает «Минутку...», чтобы человек видел: команда принята, идёт работа. Поход в сеть занимает время — без такого сообщения кажется, что бот завис.
try / exceptоборачивает поход в сеть: внешний сайт может не ответить или прислать кривой XML. Без этой защиты одна ошибка уронит хэндлер. С ней — бот спокойно извинится.disable_web_page_preview=Trueотключает автоматические превью ссылок. Иначе Telegram под каждой из пяти ссылок развернёт большую картинку-превью, и сообщение растянется на весь экран.
Частые ошибки и подводные камни
1. Перепутал .json() и .text() для RSS
RSS приходит как XML — это обычный текст, а не JSON. Если по привычке вызвать await response.json(), aiohttp попытается разобрать XML как JSON и упадёт с ошибкой вроде «не удалось декодировать JSON». Для RSS всегда бери await response.text(), а уже потом скармливай текст в ET.fromstring(). И наоборот: для JSON-API нужен .json(), не путай форматы.
2. Берёшь всю ленту и заваливаешь чат
В RSS-ленте часто 30–50 свежих статей. Если отправить их все, получится простыня на десять экранов, да ещё и Telegram может обрезать слишком длинное сообщение (предел около 4096 символов). Всегда ограничивай выборку — пять-десять новостей за глаза. Для этого и нужен limit с break в цикле или срез [:5].
3. Падение из-за пустого тега
Иногда у новости нет тега <title> или он пустой. Тогда item.find("title") вернёт None, а попытка взять None.text уронит бота с ошибкой AttributeError. Подстрахуйся: проверяй результат find перед тем, как брать .text, или используй запасное значение, например title = (el.text if (el := item.find("title")) is not None else "Без заголовка").
4. Нет обработки сетевых ошибок
Внешний сайт — не твой: он может лежать, тормозить или вернуть мусор. Если не обернуть запрос в try / except, любой такой сбой превратится в ошибку прямо посреди хэндлера, и пользователь увидит молчание вместо ответа. Сетевые походы наружу всегда оборачивай в try / except и показывай человеку понятное «не получилось, попробуй позже».
5. Забыл отключить превью ссылок
Мелочь, которая бесит. Если не поставить disable_web_page_preview=True, Telegram под каждой ссылкой развернёт картинку и описание сайта. Пять новостей превратятся в гигантскую ленту превью, в которой не видно самих заголовков. Один параметр — и сообщение снова компактное.
Мини-практика: новости по теме
Теперь твой ход. Прокачай команду /news так, чтобы можно было попросить новости на конкретную тему.
- Пользователь пишет
/news игры— бот присылает только те новости, в заголовке которых встречается слово «игры» (без учёта регистра). - Если после команды ничего не написали (просто
/news) — бот, как и раньше, шлёт всю свежую подборку. - Если по теме ничего не нашлось — бот честно отвечает «По теме „игры“ пока ничего нет».
Подсказки: текст после команды можно достать из message.text — отрежь от него саму команду и получишь тему. Чтобы сравнивать без учёта регистра, приводи и заголовок, и тему к нижнему регистру через .lower(), а проверку делай оператором in: theme in title.lower(). Фильтровать удобно прямо при сборке списка новостей.
Когда заработает — попробуй усложнить: команда /sources, которая ходит сразу за двумя разными RSS-лентами и присылает смешанную подборку. Подсказка: вызови fetch_news() для каждого адреса и сложи списки через +.
Итоги
Сегодня «Цыплёнок-помощник» научился приносить свежие новости — настоящий мини-агрегатор в кармане. Что у тебя теперь в арсенале:
- Два источника ленты: новостной API (отдаёт
JSON) и RSS (отдаётXML); RSS обычно бесплатен и не требует ключа; - Парсинг JSON — достаём список статей по ключу и идём по нему циклом, забирая
titleиurl; - Парсинг XML через
xml.etree.ElementTree:ET.fromstring(),root.iter("item")иitem.find("title").text; - Поход за лентой через
aiohttpсawait response.text()и ограничением количества новостей; - Сборка и отправка подборки одним сообщением с
disable_web_page_preview=Trueи защитой черезtry / except.
Главная мысль урока: парсинг — это не магия, а аккуратный разбор ответа на нужные кусочки. Неважно, JSON это или XML, — в итоге ты просто проходишь циклом по статьям и достаёшь из каждой заголовок и ссылку.
В следующих уроках раздела мы научим бота не ждать команды, а присылать новости сам — по расписанию, например каждое утро. Для этого подружим его с планировщиком задач. Так наш «Цыплёнок» из «принеси по запросу» превратится в «сам напомню вовремя». До встречи!