Пишем telegram-бота с задачками на Python и BS4

Одним днём мы, редакция pythonist.ru, от нечего делать, стали кидать друг другу задачки и смотреть, кто быстрее решит. В какой-то момент нам пришла в голову идея автоматизировать этот процесс. Нам понадобился бот, который отправлял бы нам случайные задачки, а мы бы уже их наперегонки решали.

Итак, что мы имеем:

  • Наша редакция предпочитает общение в telegram
  • Мы все пишем на Python

Следовательно, нам нужно написать на python что-то, что будет отправлять нам задачки прямо в чат. Источником задач мы, конечно же, взяли наш цикл статей по проекту Эйлера. Он ещё только в процессе заполнения, но его вполне можно использовать для наших задач.

Теперь, нужно разобраться с библиотеками, которыми мы будем пользоваться при написании бота. Ими станут:

  • pytelegrambotapi — основная библиотека для написания самого бота
  • beautifulsoup4 — для парсинга сайта и обработки ссылок на задачи

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

pip install pytelegrambotapi
pip install beautifulsoup4

Как мы видим, библиотеки установлены, и можно продолжать начатое. Следующим шагом мы создадим самого бота. Сделаем это с помощью @BotFather в самом telegram.

Отлично, бот создан, самое веселое ждет нас впереди, начинаем писать бота. Где писать — выбор каждого, главное, что внутри.

В последнем сообщении от BotFather, мы получили токен нашего бота, им нельзя делиться, так как это ключ к боту, позволяющий делать с ним всё что угодно.

Для начала нам нужно подключить бота к нашему python-коду, напишем следующее:

import telebot
TOKEN = 'СЮДА ПИШЕМ ТОКЕН'
bot = telebot.TeleBot(TOKEN)

Таким образом, наш скрипт будет понимать, с чем он работает, и отправлять запросы именно через нашего бота. Повторюсь, что очень важно не показывать никому свой токен, это может стоить вам бота.

Для того, чтобы убедиться, что всё в порядке, напишем небольшую функцию, для обработки команды ‘/start’. Telebot предоставляет удобные инструменты для обработки сообщений, собственно поэтому мы его и используем.

Итак, напишем декоратор, а потом разберемся, что к чему.

@bot.message_handler(commands=['start'])
def command_hello(message):
    bot.reply_to(message, "Привет, если вы видите это сообщение, значит я работаю так, как надо:)")
while True: # Для постоянной работы
    bot.polling()

Честно, даже с первого раза получилось. А теперь давайте разбираться что и как работает.

Первой строкой мы обратились к декоратору message_handler, он обрабатывает все входящие сообщения, если не передать ему никаких параметров. Мы же передали ему commands=[‘start’]. Это значит, что он будет реагировать только на сообщения-команды (начинающиеся со слэша), а в нашем случае, только на команду /start. Другие сообщения его не интересуют.

Затем мы прописываем функцию, которую декорируем, и говорим нашему боту, чтоб отвечал на сообщение ‘/start’ неким сообщением.

Последние две строчки нужны для того, чтобы бот работал постоянно, пока запущен. Просто оборачиваем bot.polling() в бесконечный цикл.

Бот работает, надо разобраться с тем, как мы будем получать задачи.

Для этих целей отлично подойдет BS4, который мы сразу установили. Это жутко удобная библиотека для парсинга совершенно любых сайтов, все зависит от скилла.

Но перед тем, как отдать сайт на съедение beautifulsoup, мы должны получить его html-код, делается это очень просто, импортируем встроенную библиотеку urllib, и отдаем ей url нашего сайта.

import urllib
site = urllib.request.urlopen(‘https://pythonist.ru/spisok-zadach-proekt-ejlera-s-resheniyami/‘).read()

Передадим библиотеке beautifulsoup наш html-код, записанный в переменную site и, обработав улучшалкой beautifulsoup.prettify(), выведем полученный результат, чтобы убедиться, что все идет по плану.

soup = bs4.BeautifulSoup(site)
print(soup.prettify())

На выводе мы получим огромное полотно кода, среди которого нас интересует только вот этот кусок:

<div class="entry-content">
<p>
<a href="https://pythonist.ru/zadacha-1-proekt-ejlera/">
Задача 1 «Числа, кратные 3 или 5»
</a>
<br/>
<a href="https://pythonist.ru/zadacha-2-chetnye-chisla-fibonachchi/">
Задача 2 «Четные числа Фибоначчи»
</a>
<br/>
<a href="https://pythonist.ru/zadacha-2-summa-czifr-faktoriala/">
Задача 20 «Сумма цифр факториала»
</a>
<br/>
<a href="http://pythonist.ru/zadacha-21-druzhestvennye-chisla/">
Задача 21 «Дружественные числа»
</a>
<br/>
<a href="http://pythonist.ru/zadacha-23-neizbytochnye-summy/">
Задача 23 «Неизбыточные суммы»
</a>
<br/>
<a href="http://pythonist.ru/zadacha-24-slovarnye-perestanovki/">
Задача 24 «Словарные перестановки:
</a>
</p>
</div>
<!-- .entry-content -->

Именно тут хранятся нужные нам ссылки на задачи, а мы находимся всё ближе к своей цели. Теперь нам нужно достать эти ссылки, чтобы у нас была возможность отправлять их. Для этого немного переписываем предыдущий код, не переживайте, без объяснений не останетесь:)

site = urllib.request.urlopen('https://pythonist.ru/spisok-zadach-proekta-ejlera-s-resheniyami/').read()
soup = bs4.BeautifulSoup(site)
raw_excersises = soup.find('div', {"class":'entry-content'}) #забираем интересующий нас кусок кода
excersises = raw_excersises.find_all('a')
links_to_excersises = []
for i in range(len(excersises)):
    links_to_excersises.append(excersises[i].get('href'))
print('I have a list')

Что происходит в этом коде:

  • забираем html код сайта
  • скармливаем этот код bs4
  • находим нужный нам фрагмент кода, в котором хранятся ссылки
  • забираем непосредственно блоки с ссылками
  • в цикле for собираем список, состоящий только из ссылок
  • убеждаемся, что код выполнился

У этого кода есть один недостаток — при добавлении новой задачи на сайт, придется перезапускать бота, чтобы он заново спарсил список. Но так он работает намного быстрее, так как парсинг — дело долгое.

Ну все, осталось самое простое — по запросу выбрасывать ссылку на случайную задачу в чат. Для этого напишем декоратор-обработчик команды, назовем ее /task.

import random
@bot.message_handler(commands=['task'])
def send_task(message):
    link_to_send = random.choice(links_to_excersises)
    bot.reply_to(message, f'Окей, решайте вот эту задачу — {link_to_send}')

Тут особо сложного ничего нет, пройдемся по порядку:

  • импортируем модуль random, он нужен для выбора случайной статьи
  • задаем обработчику параметр, обеспечивающий работу только при сообщении /task
  • выбираем ссылку, которую будем отправлять
  • отправляем эту ссылку

Мы молодцы, бот написан и работает прекрасно. Однако, вы можете улучшить действующий функционал. Для закрепления материала попробуйте, например, сделать так, чтобы бот отправлял не ссылку на задачу, а сам текст задачи. Мы уверены, что вы справитесь. Ну и куда же без полного кода нашего бота, развлекайтесь!

import telebot
import bs4
import urllib
import random
site = urllib.request.urlopen('https://pythonist.ru/spisok-zadach-proekta-ejlera-s-resheniyami/').read()
soup = bs4.BeautifulSoup(site)
raw_excersises = soup.find('div', {"class":'entry-content'}) #забираем интересующий нас кусок кода
excersises = raw_excersises.find_all('a')
links_to_excersises = []
for i in range(len(excersises)):
    links_to_excersises.append(excersises[i].get('href'))
print('I have a list')
TOKEN = 'да-да, тут должен быть токен'
bot = telebot.TeleBot(TOKEN)
@bot.message_handler(commands=['start'])
def command_hello(message):
    bot.reply_to(message, "Привет, если вы видите это сообщение, значит я работаю так, как надо:)")
@bot.message_handler(commands=['task'])
def send_task(message):
    link_to_send = random.choice(links_to_excersises)
    bot.reply_to(message, f'Окей, решайте вот эту задачу — {link_to_send}')
while True: # Для постоянной работы
    bot.polling()