Каждый день мы просматриваем habr. Каждый день заходим на главную ленту и крутим её. Что, если автоматизировать этот просмотр?
В статье я расскажу, как я писал telegram-бота на python3, который вытаскивает заголовки статей с habr и пишет их в telegram.
Как это реализовать?
У python3 есть библиотека – beautiful soap 4. С помощью этой библиотеки можно парсить сайты.
Парсинг (parsing) — это сбор информации из сторонних источников и сайтов для использования полученных данных в различных целях.
Простыми словами, я пишу скрипт, который вытаскивает из сайта какую-то информацию (в нашем случае заголовки статей). Подробнее о парсинге сайтов можно найти здесь.
Как можно видеть по “этому” скрину,

Каждая статья в хабре состоит из тэгов “ article” со своим id.
Можно просто уменьшать переменную, пока она не совпадет с id. Этот способ очень долгий. Надо ждать, пока python3 выполнит ~700 000 итераций!
Есть другой вариант. Можно найти все тэги “ article”, отвечающие за статьи, и поочереди перебирать эти тэги. Цикл останется, но будет делать уже 20(столько статей на глав. экране) итераций.
from itertools import count from re import I import requests from bs4 import BeautifulSoup url2 = "https://habr.com/ru/all/" response2 = requests.get(url2) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag[i].find("h2").find("span").text print(out + " url: " + "https://habr.com" + urlOut)
Теперь приклеевыем этот кусок кода к pyTelegramBotAPI:
import requests from bs4 import BeautifulSoup import telebot bot = telebot.TeleBot("TOKEN") url2 = "https://habr.com/ru/all/" response2 = requests.get(url2) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") @bot.message_handler(commands=["start"]) def start(m, res=False): bot.send_message(m.chat.id, "Bot is started.") @bot.message_handler(commands=["habr"]) def habr(message): global url2 response2 = requests.get(url2) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag[i].find("h2").find("span").text bot.send_message(message.chat.id, out) bot.polling(none_stop=True, interval=0)
Очень важно прикрепить эти строчки в фунцию habr, т.к. если этого не сделать, наш бот не будет обновлятся.
Эти строчки:
response2 = requests.get(url2) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item")
Это пока не совсем удобно! Ссылки-то нету!
Проблему со ссылкой легко исправить. Надо просто добавить эту строчку в функцию “habr”:
urlOut = "https://habr.com" + tag[i].find("h2").find("a").get("href")
И исправить вывод с "out" на "out + " url: " + urlOut".
Вот, что получается в telegram:

Вывод слишком резкий получается. Надо сделать задержку между каждым выводом.
Импортируем sleep из time:
from time import sleep
И добавить задержку в цикл:
for i in range(0, 10): out = tag[i].find("h2").find("span").text urlOut = "https://habr.com" + tag[i].find("h2").find("a").get("href") bot.send_message(message.chat.id, out + "url: " + urlOut) sleep(5)
В telegram всё тоже-самое, но между каждой статьей – задержка.
И добавим новости:
@bot.message_handler(commands=["news"]) def habr(message): global url response2 = requests.get(url) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag[i].find("h2").find("span").text urlOut = "https://habr.com" + tag[i].find("h2").find("a").get("href") bot.send_message(message.chat.id, out + "url: " + urlOut) sleep(5)

Знаете, что я вам скажу? Это опять неудобно! А что, если я не захочу читать дальше? Нужна кнопка, отвечающая за стоп. Поизучав, как это работает, я нашел и url-кнопку, которая отвечает за переход на сайт. Отдельная кнопка красивее, чем тупо ссылка! Короче, делаем две Inline кнопки.
Для начало нужно добавить в “habr” и “news” эти строки:
markup = types.InlineKeyboardMarkup(row_width=2) btn_url = types.InlineKeyboardButton(text="Go to habr.", url=urlOut) btn_stop = types.InlineKeyboardButton(text="STOP IT!", callback_data="stop") markup.add(btn_url, btn_stop)
и для “habrNews” :
markup = types.InlineKeyboardMarkup(row_width=2) btn_url = types.InlineKeyboardButton(text="Go to habr.", url=urlOut) btn_stop = types.InlineKeyboardButton(text="STOP IT!", callback_data="stop_news") markup.add(btn_url, btn_stop)
и добавить аргумент в bot.send_message. Теперь он выглядит так:
bot.send_message(message.chat.id, out + "url: " + urlOut, reply_markup=markup)
Теперь добавим обработчик события callback кнопки:
@bot.callback_query_handler(func=lambda call:True) def call_repley(call): if call.message and call.data == "stop": None
И вот тут я прям встал. Я не знал, что делать! Мне надо было передать сообщение из “call_repley” в “habr”. В habrЕ умные дядьки пишут, что лучшим вариантом будет передать это через callback_data, но я 12-ти летний пацан! Мне показалось слишком сложным эта схема. Дело в том, что умные дядьки из habrА перед тем, как пишут через callback_data, пишут через глобальные переменные. Я не понимал, как работают глабальные переменные до того, как встретил эту статью. Спасибо, DanyByLuckyCraft!
И так, пишем через глобальные переменные:
isCall = False @bot.callback_query_handler(func=lambda call:True) def call_repley(call): global isCall if call.message and call.data == "stop": isCall = True @bot.message_handler(commands=["habr"]) def habr(message): global isCall
И обрабатываем событие в “habr”:
if isCall: break
Как это выглядит:

Двигаем дальше.
Теперь надо сделать ещё одну кнопку, чтобы листать вперед. Смотря на ту-же статью, я осознал, что можно перелистывать страницы, вместо того, чтобы высылать их поочередно. Если мы будем перелистывать страницы, можно кнопку “STOP IT!” убрать. И если мы будем перелистывать страницы, не будет возможности посмотреть предыдущие статьи. Надо добавить кнопку “назад”.
В этот раз функция “habr” полностью переделывается:
@bot.message_handler(commands=["habr"]) def habr(message): global page global pages page = 0 pages = [] response = requests.get(url2) response.raise_for_status() soup = BeautifulSoup(response.text, "lxml") tag2 = soup.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag2[i].find("h2").find("span").text urlOut = "https://habr.com" + tag2[i].find("h2").find("a").get("href") pages.append([out, urlOut]) markup = markUP(pages=pages, page=page) bot.send_message(message.chat.id, pages[page][0], reply_markup=markup)
Теперь объясню. Я создал переменные “pages” и “page”, которые отывечают за перелистывание страниц. В (уже список) “pages” каждую итерацию я добавляю еще список из переменных out и urlOut. И переменная “page” отвечает за индекс конкретного элемента [out, urlOut] в “pages”.
В коде можно заметить строчку
markup = markUP(pages=pages, page=page)
Она отвечает за присвоения переменной “markup” некой функции “markUP”. Я сделал отдельную функцию “markUP”:
def markUP(pages, page): markup = types.InlineKeyboardMarkup(row_width=2) btn_url = types.InlineKeyboardButton(text="Go to habr.", url=pages[page][1]) btn_next = types.InlineKeyboardButton(text="Next page.", callback_data="habr_next") btn_back = types.InlineKeyboardButton(text="Back page.", callback_data="habr_back") markup.add(btn_back, btn_next, btn_url) return markup
Я просто взял эти сточки
markup = types.InlineKeyboardMarkup(row_width=2) btn_url = types.InlineKeyboardButton(text="Go to habr.", url=urlOut) btn_stop = types.InlineKeyboardButton(text="STOP IT!", callback_data="stop_news") markup.add(btn_url, btn_stop)
и перенес их в функцию, чтобы не повторять их 4 раза.
call_reply тоже пришлось переделать:
@bot.callback_query_handler(func=lambda call:True) def call_repley(call): global page global pages if call.message and call.data == "habr_back": page -= 1 try: markup = markUP(pages=pages, page=page) bot.edit_message_text(pages[page][0], reply_markup = markup, chat_id=call.message.chat.id, message_id=call.message.message_id) except: bot.answer_callback_query(call.id, show_alert=True, text="Такой статьи нету.") page += 1 if call.message and call.data == "habr_next": page += 1 try: markup = markUP(pages=pages, page=page) bot.edit_message_text(pages[page][0], reply_markup = markup, chat_id=call.message.chat.id, message_id=call.message.message_id) except: bot.answer_callback_query(call.id, show_alert=True, text="Такой статьи нету.") page -= 1
После нажатия кнопки, первое сообщение редактируется и заменяется другим, с другим значением “page” или другими [out, urlOut]. Конструкция try-except нужна, чтобы значение page не вышло за рамки “pages”(короче, чтобы ошибка “IndexError: list index out of range” не появилась).
Теперь не забываем про “habrNews”:
@bot.message_handler(commands=["news"]) def habrNews(message): global page global pages page = 0 pages = [] response2 = requests.get(url) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag[i].find("h2").find("span").text urlOut = "https://habr.com" + tag[i].find("h2").find("a").get("href") pages.append([out, urlOut]) markup = markUP(pages=pages, page=page) bot.send_message(message.chat.id, pages[page][0], reply_markup=markup)
Ну вот и всё!
Полный код:
from re import T import requests from bs4 import BeautifulSoup import telebot from telebot import types bot = telebot.TeleBot("TOKEN") url = "https://habr.com/ru/news/" url2 = "https://habr.com/ru/all/" response = requests.get(url) response.raise_for_status() response2 = requests.get(url2) response2.raise_for_status() soup = BeautifulSoup(response.text, "lxml") tag2 = soup.find_all("article", class_="tm-articles-list__item") soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") @bot.message_handler(commands=["start"]) def start(m, res=False): bot.send_message(m.chat.id, "Bot is started.") page = 0 pages = [] def markUP(pages, page): markup = types.InlineKeyboardMarkup(row_width=2) btn_url = types.InlineKeyboardButton(text="Go to habr.", url=pages[page][1]) btn_next = types.InlineKeyboardButton(text="Next page.", callback_data="habr_next") btn_back = types.InlineKeyboardButton(text="Back page.", callback_data="habr_back") markup.add(btn_back, btn_next, btn_url) return markup @bot.callback_query_handler(func=lambda call:True) def call_repley(call): global page global pages if call.message and call.data == "habr_back": page -= 1 try: markup = markUP(pages=pages, page=page) bot.edit_message_text(pages[page][0], reply_markup = markup, chat_id=call.message.chat.id, message_id=call.message.message_id) except: bot.answer_callback_query(call.id, show_alert=True, text="Такой статьи нету.") page += 1 if call.message and call.data == "habr_next": page += 1 try: markup = markUP(pages=pages, page=page) bot.edit_message_text(pages[page][0], reply_markup = markup, chat_id=call.message.chat.id, message_id=call.message.message_id) except: bot.answer_callback_query(call.id, show_alert=True, text="Такой статьи нету.") page -= 1 @bot.message_handler(commands=["habr"]) def habr(message): global page global pages page = 0 pages = [] response = requests.get(url2) response.raise_for_status() soup = BeautifulSoup(response.text, "lxml") tag2 = soup.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag2[i].find("h2").find("span").text urlOut = "https://habr.com" + tag2[i].find("h2").find("a").get("href") pages.append([out, urlOut]) markup = markUP(pages=pages, page=page) bot.send_message(message.chat.id, pages[page][0], reply_markup=markup) @bot.message_handler(commands=["news"]) def habrNews(message): global page global pages page = 0 pages = [] response2 = requests.get(url) response2.raise_for_status() soup2 = BeautifulSoup(response2.text, "lxml") tag = soup2.find_all("article", class_="tm-articles-list__item") for i in range(0, 10): out = tag[i].find("h2").find("span").text urlOut = "https://habr.com" + tag[i].find("h2").find("a").get("href") pages.append([out, urlOut]) markup = markUP(pages=pages, page=page) bot.send_message(message.chat.id, pages[page][0], reply_markup=markup) bot.polling(none_stop=True, interval=0)
Весь код также можно найти в githubЕ.
Я забыл сказать, как этого бота сделать через fatherBot, но это и так все знают.
Как я уже упомянул, мне 12 лет. Это значит, что программировать и создавать ботов могут все!
Спасибо за внимание!