Как стать автором
Обновить

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

Время на прочтение8 мин
Количество просмотров6.7K

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

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

Теги:
Хабы:
Всего голосов 19: ↑13 и ↓6+8
Комментарии26

Публикации

Истории

Работа

Python разработчик
113 вакансий
Data Scientist
60 вакансий

Ближайшие события