Telegramm-habr-бот. Долгий путь к совершенству

от автора

Каждый день мы просматриваем 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 лет. Это значит, что программировать и создавать ботов могут все! 

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/post/686174/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *