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

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

Комментарии (20)


  1. Expany
    03.09.2022 01:12
    +1

    Telegram - с одной M.
    Больше того, у Хабра есть целый пул каналов, и жалких пародий на них с рекламой и спамом, в телеграм.


    1. pewpew
      03.09.2022 03:04
      +5

      Вы слишком предвзяты к юному дарованию и его первой статье из песочницы.
      Я в свои 12 лет играл в денди, смотрел мультики про черепашек ниндзя и трансформеров, а на уроке информатики играл в принца персии и рисовал довольно посредственные домики в qbasic и даже подумать не мог, во что перерастёт увлечение компьютерами.
      Впрочем уже в 14 лет мог переустановить винду, собрать комп и баловался с ассемблером. Но то же в 14 лет. А тут вполне годный бот, проект для обучения, способный принести практическую пользу. Именно на реальных проектах и учатся.
      Желаю успехов!


    1. daniil12332 Автор
      03.09.2022 20:58
      +1

      Telegram и должна быть с одной М. Так на официальном сайте написано.


    1. stal104
      05.09.2022 14:30

      Ну 12 лет же! Я в этом возрасте строил шалаши, гонял кораблики по ручьям, добивал родительский велик и ждал мультики по утрам в выходные. Немного мечтал о первой соньке.


  1. propulsive
    03.09.2022 04:04
    +10

    Респект за решение задачи. Только зачем парсить, когда есть RSS? https://habr.com/ru/docs/help/lenta/


    1. delphinpro
      03.09.2022 05:49
      +4

      Тоже первой мыслью в начале статьи было напомнить в комментариях про RSS.
      Но оказалось автору всего 12 лет. Задача поставлена и выполнена, статья оформлена неплохо. Достойно плюсика.


    1. rg_ceo
      03.09.2022 09:49
      +1

      Спасибо за инфу. Хотел сделать нечто похожее с функцией RSS на botmother конструкторе.


    1. MechanicusJr
      03.09.2022 09:56
      +1

      Только зачем парсить, когда есть RSS

      RSS есть не на всех сайтах. Иногда надо парсить вообще тупой текст из мега портянки на 50-500 килобайт.

      На некоторых сайтах зато есть апи.


      1. propulsive
        04.09.2022 03:35
        +1

        В данном случае он (RSS) есть, мы же не пишем какой-то универсальный парсер для всех сайтов


    1. daniil12332 Автор
      03.09.2022 13:23
      +1

      Спасибо за решение. На то время, когда я делал бота, я не знал про «RSS».


  1. propulsive
    03.09.2022 04:30

    В R это делается ещё проще: 1) получаем RSS-ленту 2) преобразуем датафрейм в JSON 3) сохраняем файл см. пример https://rpubs.com/tukachev/937551


  1. rg_ceo
    03.09.2022 09:49

    Интересная идея


  1. MechanicusJr
    03.09.2022 09:55
    +2

    Как я уже упомянул, мне 12 лет. Это значит, что программировать и создавать ботов могут все! 

    Для 12 лет отлично!

    Следующие шаги:

    • записывать логи каждой операции - если скрипт будет выполняться как сервис и с отладкой, кроме как по логам, будет никак

    • Записывать массив данных (xml например) в файл, для того чтобы в следующий заход скрипт считал файл и не слал данные по второму разу

    • Сделать универсальный файл для такого же парсинга несколькх сайтов


    1. daniil12332 Автор
      03.09.2022 13:28
      +1

      Спасибо за идеи. Я не всё понял, но всё равно спасибо!


  1. freedoomer
    04.09.2022 12:08

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

    респект


    1. daniil12332 Автор
      04.09.2022 12:08

      Спасибо


  1. webalex127
    05.09.2022 14:22

    Надеюсь токен невалидный https://github.com/daniil12332/telebot/blob/main/butsoup.py#L7


  1. inklesspen
    05.09.2022 14:23

    Хабр мне всегда казался чем-то вроде журнала, где выкладывают научный, близко стоящий или просто полезный материал. Конечно, написать бота в 12 лет - хорошо, написать рабочего бота - еще лучше (без шуток, не у каждого получается, даже если склад ума вроде бы соответствует), но, как мне кажется, статье следует быть не на хабре (и, видимо, не мне одному кажется, т.к. судя по комментариям, плюсы ставили, но оценка у статьи 0, значит, столько же минусов).

    Для меня хабр это место, где люди могут поделиться своими (и не совсем) открытиями, новостями, прямо или косвенно относящимся к хабу, интересными мыслями, практиками и т.д. Всё перечисленное объединяет полезность (как, к примеру, статья, на которую ссылался автор). Ну или у меня не тот образ хабра сложился.

    Из этой статьи я узнал, что, почему-то, asyncio все еще не везде используется. С одной стороны, он тут пока не нужен, да и бот спроектирован для работы одновременно с одним пользователем, с другой - не все боты так используются, а асинхронность полезна для развития. К слову, в статье, на которую ссылается автор, предусмотрена работа с несколькими пользователями.

    Я, конечно, рад, что вы умелы в столь юном возрасте, но ваша задача (или, по крайней мере, её исполнение), по моему мнению, не тянет на статью. За грамматику хвалю, на уровне.


  1. boopiz
    05.09.2022 14:30

    Отлично! Теперь для саморазвития раскручивай для себя очереди сообщений и попробуй сделать демона, который будет по разным тематикам раскидывать новые заголовки по разным топикам, а в телеге сделать несколько гпупп и демона на пайтоне, с настройками топик->id_группы который будет подписан на разные топики и будет публиковать новые сообщения в свой канал.


  1. kot123123
    05.09.2022 14:30

    Вы допустили 2 огромные ошибки, которые влияют на безопасность.

    1. Вы забыли убрать ключ апи на Гите

    2. Не храните никогда такие данные в скриптах, храните в переменных окружения. Вот референс