Pull to refresh

BIK Beep – Telegram Bot

Reading time22 min
Views11K
Логотип бота
Логотип бота

Содержание


Постановка задачи и первичные варианты решения

В связи с ежедневными вечерними (да ещё и постоянно в разное время) обновлениями расписания в ОГАПОУ «Белгородский индустриальный колледж» необходимо программное обеспечение (ПО), которое будет следить за расписанием и уведомлять при его изменении.

Среди вариантов решения были рассмотрены:

  • собственное андроид-приложение;

  • бот/сервис в социальной сети/мессенджере.

В начале разработки приложения под андроид в Android Studio возникла проблема работы фоновой службы без запущенного приложения на операционной системе (ОС) Android 11 (API 30).

Данная проблема будет решена мной позже.

Бот или сервис для социальной сети «ВКонтакте» не разрабатывался, но начало разработки в ближайших планах.

Да, в Интернете по созданию ботов в Telegram есть куча статей, но в данной статье будет описан весь мой путь до стабильной и бесплатной работы бота в мессенджере Telegram.


Сервисы для разработки

Официальные логотипы используемых сервисов
Официальные логотипы используемых сервисов

Python

Языком программирования был выбран Python 3.10.0 – высокоуровневый язык программирования общего назначения. Данный язык программирования лёгок в освоении, поэтому подойдёт как начинающим, так и более опытным программистам. Главное, что необходимо знать про Python:

  • конец строки является концом инструкции;

  • вложенные инструкции объединяются в блоки по величине отступов;

  • вложенные инструкции – это инструкции, которые следуют под основной инструкций, которая завершается двоеточием.

Немного документации по Python:

На текущий момент уже представлен Python 3.11, находящийся пока в разработке.

Для удобства дальнейшей работы необходимо проверить, чтобы в системной переменной PATH был путь до директории Python.

%UserProfile%\AppData\Local\Programs\Python\Python310

Для установки пакетов Python будет использоваться инструмент pip v21.3.1. Для удобства дальнейшей работы этого инструмента необходимо проверить, чтобы в системной переменной PATH был путь до директории скриптов Python.

%UserProfile%\AppData\Local\Programs\Python\Python310\Scripts

Visual Studio Code

Редактором кода пользовался Visual Studio Code v1.62.3 (VS Code) от Microsoft. Редактор для языка Python, как и для многих других языков программирования, поддерживает удобную функцию IntelliSense – вспомогательное средство завершения слов и завершения кода, а также выводит сведения по работе с функциями и методами.

Heroku

Развернём бота на облачной платформе Heroku. Heroku – это одна из первых облачных платформ, которая появилась в 2007 году. На серверах используются Unix-подобные ОС. Бесплатным аккаунта предоставляется 550 часов каждый месяц. Подтвердив свою личность с помощью кредитной карты можно получить дополнительно 450 часов в месяц. В общей сложности будет 1000 бесплатных часов в месяц.

Для создания и управления приложениями Heroku из командной строки необходим Heroku Command Line Interface (CLI) v7.59.2.

Справка по основным командам Heroku CLI:

Для удобства дальнейшей работы необходимо проверить, чтобы в системной переменной PATH был путь до директории Heroku.

C:\Program Files\heroku\bin

Git

Также для развёртки бота на Heroku необходима система управления версиями Git v2.34.1. Cистемы управления версиями отслеживают изменения программного кода для дальнейшего управления. Контроль версий помогает командам разработчиков предотвращать возникновение конфликтов при параллельной работе путем отслеживания каждого изменения, внесенного каждым участником.

Документация по Git:

Для удобства дальнейшей работы необходимо проверить, чтобы в системной переменной PATH был путь до директории Git.

C:\Program Files\Git\cmd

PostgreSQL

В качестве базы данных (БД) будем использовать PostgreSQL v14.1 – мощная система управления БД (СУБД), базируемая на языке SQL. Платформа Heroku предоставляет сервис Heroku Postgres для взаимодействий приложений, развёрнутых на Heroku, с БД PostgreSQL.

Документация по PostgreSQL и языку SQL:

pgAdmin 4

Инструментом для администрирования БД PostgreSQL выступит pgAdmin 4 v6.2. Данный инструмент позволяет выполнять как SQL запросы, так и мониторинг БД.

Документация по pgAdmin 4:

Telegram

Всё вышеперечисленное необходимо для стабильной и бесплатной работы будущего бота в Telegram.

Необходимая документация по ботам в Telegram:

API (Application Programming Interface – программный интерфейс приложения) – описание методов, которыми другое ПО может взаимодействовать с этим ПО.


Описание используемых пакетов Python

AIOgram

Бот будет написан не на чистом Telegram Bot API, а с помощью модуля AIOgram v2.16 – простого и асинхронного фреймворка, написанного на Python 3.7. Модуль уже поддерживает версию Telegram Bot API 5.5 вышедшую 7 декабря 2021 года.

Документация по AIOgram:

Также у модуля есть версия в разработке AIOgram v3, написанный уже на Python 3.8. На момент написания статьи AIOgram v3 поддерживает только Telegram Bot API 5.4

Документация по AIOgram v3:

Но нужно помнить, что любое ПО в разработке не гарантирует стабильную работу.

asyncio

Асинхронную работу будет обеспечивать asyncio – модуль стандартной библиотеки Python. Асинхронное программирование – это особенность современных языков программирования, которая позволяет выполнять операции, не дожидаясь их завершения.

Наглядное сравнение синхронного и асинхронного выполнения задач
Наглядное сравнение синхронного и асинхронного выполнения задач

Документация по asyncio:

Requests

GET запрос на получение данных Интернет страницы выполним с помощью библиотеки Requests v2.26.0 – простой модуль для отправки всех видов запросов HTTP/1.1, разработанного в 1997 году. Модуль Requests поддерживает Python 2.7 и 3.6+. На текущий момент в стадии черновика уже находится HTTP/3.

Документация по Requests:

Немного документации о протоколе HTTP и запросах:

Beautiful Soup

Для извлечения данных из HTML файлов использована библиотека Beautiful Soup v4.10.0 – простой парсер, работающий на Python 3.8. Поддержка Beautiful Soup 3 прекращена начиная с 2021 года. Beautiful Soup v4.9.3 последняя версия, поддерживающая Python 2.7.

Документация по Beautiful Soup 4:

os

Для работы с ОС воспользуемся одноимённым модулем os – пакетом из стандартной библиотеки Python. Модуль позволяет работать с файловой системой, управлять процессами. Вложенным модулем в os является модуль os.path для работы с путями.

Документация по os и os.path:

Нужно понимать, что некоторые функции из модуля поддерживаться не всеми ОС.

HTML2Image

Преобразование HTML кода страницы в изображение произведём с помощью пакета HTML2Image v2.0.1 – оболочка безголового режима браузеров. Как следует из названия, безголовый режим браузера – это полноценный браузер без графического интерфейса.

Документация по HTML2Image:

Pillow

Для работы с растровыми изображениями воспользуемся библиотекой Pillow v8.4.0 – ответвлением библиотеки Python Image Library (PIL), разработка которой прекращена. Растровая графика состоит из сетки пикселей, но разрешение изображений такое, что отдельные пиксели неразличимы, в отличии от пиксельной графики. Также растровая графика теряет качество при масштабировании.

Разница между растровой и векторной графикой при масштабировании
Разница между растровой и векторной графикой при масштабировании
Разница между растровой и векторной графикой при масштабировании
Разница между растровой и векторной графикой при масштабировании
Разница между растровой и векторной графикой в структуре
Разница между растровой и векторной графикой в структуре

Документация по Pillow:

psycopg2

Для работы с БД PostgreSQL воспользуемся модулем psycopg2 v2.9.2 – адаптер БД PostgreSQL. Psycopg 2 разработан на C, как оболочка libpq. Поддерживает Python 3.

Документация по Psycopg 2:

Также у модуля есть версия в разработке psycopg3. Модуль поддерживает Python 3.6-3.10 и PostgreSQL v10-v14.

Документация по Psycopg 3:

Но нужно помнить, что любое ПО в разработке не гарантирует стабильную работу.


Сборочные пакеты Heroku

Для преобразования развёрнутого кода в служебный файл применяются сборочные пакеты – buildpacks. Heroku предоставляет официально поддерживаемы сборочные пакеты для многих языков программирования.

Официально поддерживаемые сборочные пакеты
Официально поддерживаемые сборочные пакеты

В торговой площадке Heroku на момент написания статьи имеется 8608 сборочных пакетов. В случае отсутствия необходимого сборочного пакета есть документация по разработке пользовательских сборочных пакетов – Buildpack API.


Процесс разработки бота

Создание нового бота в BotFather

Для начала необходимо создать нового бота и автоматически получить токен этого бота у создателя ботов в Telegram @BotFatherСправка по командам была представлена в пункте «Сервисы для разработки – Telegram» этой статьи. Также сразу можно изменить название бота, описание бота, информацию о боте, фото профиля бота.

Запишем полученный токен в файл конфигов configs.py:

# токен бота из @BotFather
TOKEN = '2118918752:token'

Сам токен скрыт в этой статье, чтобы предотвратить несанкционированное использование моего бота.

Создание нового приложения на Heroku

После регистрации на Herokuсоздадим новое приложение в регионе Европа. На выбор предлагался ещё регион Соединённые Штаты. Подробнее про регионы для приложений Heroku – развертка в различных географических регионах.

Окно создание нового приложения на Heroku
Окно создание нового приложения на Heroku

Классическая структура бота Telegram

Присвоим токен, инициализируем обработчик входящих сообщений. Напишем обработчик для команды /start – глобальной команды, с которой начинается общение любого пользователя с любым ботом Telegram. В конце запустим режим длительного опроса. Таким образом создадим классическую структуру бота в основном файле bot.py:

Код Python
# файл конфигов
from configs import TOKEN

# фреймворк для Telegram Bot API
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor

# присвоение токена
bot = Bot(token=TOKEN)

# инициализируем обработчик входящих обновлений
dp = Dispatcher(bot)

@dp.message_handler(commands=['start'])
async def welcome(message: types.Message):
  await message.answer('При изменении расписания СильченкоОВ будут присылаться уведомления.')

# при запуске файла
if (__name__ == '__main__'):
  # запуск режима длительного опроса
  executor.start_polling(dp, skip_updates=True)

Блок if (__name__ == '__main__'): позволяет выполнять вложенные инструкции только при запуске самого файла, а не кода из импортированного модуля. Подробнее можно прочитать в статье «Зачем нужен if __name__ == '__main__' ?».

Создание модуля парсера данных с сайта «Расписание занятий»

Сайт – «Расписание занятий»

Создадим конструктор __init__, в который будет передаваться URL расписания. Конструктор класса – метод, который автоматически вызывается при создании объекта этого класса. Хотя в Python правильнее называть конструктором метод __new__. Но принимать параметры нужно именно в методе __init__, ведь задача этого метода как раз изменить новое состояние вновь созданного экземпляра класса. Метод __init__ не должен ничего возвращать, чтобы не вызвать ошибку. Параметр self это ссылка на конкретный экземпляр класса. Для обращения к переменным экземпляра всегда нужно дописывать selfself.url. Начало создания модуля в файле BIKParser.py:

Код Python
class BIKParser:

  # конструктор
  def __init__(self, url):
    '''
      Парсер сайта bincol.ru страницы `url`
			
      ---------
      Параметры
      ---------
      * `url`: str (строка)

        Адрес страницы с расписанием
    '''
		
    self.url = url

Для удобного использования классов и методов необходимо грамотно заполнять docstring – строки документации. Заполняется в комментарии вида '''docstring''' или """docstring""" сразу после объявления класса или метода. Обратиться к такой строке документации можно через атрибут __doc__. Для заполнения таких строк применяется язык разметки – reStructuredText. Результат документации с синтаксисом выше при наведении на метод:

Документация при наведении
Документация при наведении

Вызов методов внутри класса тоже производится через self.

Реализуем метод заполнения расписания FillFileSchedule:

def FillFileSchedule(self):
  '''Заполнение файла расписания'''

  # заполнить файл старого расписания
  f = open('old_schedule.txt', 'w')
  f.write(str(self.new_schedule))
  f.close()

Реализуем метод проверки расписания CheckChange. Получаем страницу расписания, выделяем все строки таблиц. Выделяем только нужные данные из всех данных парсинга, сразу же дополняя в начале <table><tbody> и в конце </tbody></table> для формирования полноценной HTML таблицы. Далее необходимо выполнить проверку на наличие изменений в расписании по отношению к прошлому сохранённому расписанию. В случае изменения расписания необходимо перезаписать старый файл расписания и изменить переменную результата на True.

Код Python
# модуль запросов
from requests import get
# модуль парсера
from bs4 import BeautifulSoup as BS
# модуль для работы с файлами
from os.path import exists

def CheckChange(self):
  '''
    Проверка изменения расписания
			
    -------
    Возврат
    -------
    * `ResultCheck`: bool (логическая переменная)

      Результат проверки
  '''

  # парсинг страницы расписания
  h = get(self.url)
  html = BS(h.content, 'html.parser')

  # получим все строки таблиц
  new_schedule_buf = html.find_all('tr')

  # сформируем html код расписания
  self.new_schedule = ['<table><tbody>']
  for num in range(1, len(new_schedule_buf)):
    bufStr = str(new_schedule_buf[num])
    # заберём только нужные данные
    if (('Понедельник' in bufStr) or ('Вторник' in bufStr) or ('Среда' in bufStr) or ('Четверг' in bufStr) or ('Пятница' in bufStr) or ('Суббота' in bufStr) or ('Воскресенье' in bufStr) or ('<td valign="top">' in bufStr)):
      self.new_schedule.append(bufStr)
  self.new_schedule.append('</tbody></table>')

  # переменная результата проверки
  ResultCheck = False

  # проверка на наличие файла старого расписания
  if (exists('old_schedule.txt')):
    # откроем старое расписание
    old_schedule = open('old_schedule.txt', 'r').read()

    # если расписания отличаются
    if (str(self.new_schedule) != old_schedule):
      # сохраним новое расписание
      self.FillFileSchedule()

      # изменим результат проверки
      ResultCheck = True
    else:
      # создадим и заполним файл старого расписания
      self.FillFileSchedule()

      # изменим результат проверки
      ResultCheck = True
    
  # вернём результат проверки
  return ResultCheck

Для отправки картинки расписания нужно создать соответствующий метод ChangeImage.

Код Python
# модуль создания картинки из html
from html2image import Html2Image as HTI
# модуль работы с изображениями
from PIL import Image

def ChangeImage(self):
  '''Формируем изображение расписания'''

  # инициализируем метод создания изображения
  hti = HTI(output_path='/app')

  # получим изображение из html страницы
  hti.screenshot(html_str=''.join(self.new_schedule), save_as='schedule.png')

  # обрежем пустоту у изображения
  old_image = Image.open('schedule.png')
  new_image = old_image.crop(old_image.getbbox())
  new_image.save('schedule.png')

Так как бот планируется быть размещённым на Heroku, а там файлы приложения хранятся в директории app, то эту директорию необходимо указать в качестве выходного пути для модуля HTML2Image, импортированного нам в код, как HTI.

Также стоит обратить внимание на строчку из официальной документации модуля HTML2Image, которая была представлена в пункте «Описание используемых пакетов Python – HTML2Image» этой статьи:

However default flags are not used if you decide to specify custom_flags or change the value of browser.flags:

В которой говориться об отмене флагов по умолчанию при использовании пользовательских флагов или изменении значений флагов браузера. В таких случаях нужно будет не забыть вернуть флаги по умолчанию: --default-background-color=0 (объяснение флага) и --hide-scrollbars (объяснение флага).

Создание модуля по работе с БД PostgreSQL

В ресурсы приложения на Heroku необходимо добавить дополнение Heroku Postgres для добавления БД к приложению. При добавлении выбираем бесплатный план Hobby Dev.

Окно добавления БД PostgreSQL к приложению
Окно добавления БД PostgreSQL к приложению

Добавим в файл конфигов configs.py идентификатор упрощённого подключения к БД PostgreSQL:

# идентификатор упрощённого подключения к БД PostgreSQL
DB_URI = 'postgres://user:password@host:port/database'

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

Стоит обратить внимание на фразу из окна настроек БД на Heroku:

Please note that these credentials are not permanent. Heroku rotates credentials periodically and updates applications where this database is attached.

Из которой понятно, что учётные данные для подключения не постоянные и в ходе обслуживания БД Heroku меняет эти данные. Об обслуживании на электронную почту аккаунта Heroku приходит письмо такого содержания:

Your database DATABASE_URL on bikbeepbot requires maintenance. During this period, your database will become read-only. … We expect maintenance to last just a few moments depending on the size of your database. We will notify you when maintenance begins, and again once it's complete.

В письме говорится о том что необходимо провести обслуживание БД и в это время БД будет доступна только для чтения. Обслуживание будет быстрым. Во время начала и конца обслуживания будут приходить повторные письма.

Для создания таблицы в БД необходимо в pgAdmin 4 подключиться к выделенной нам БД по предоставленным учётным данным.

Главное окно создание сервера
Главное окно создание сервера
Окно настройки соединения сервера
Окно настройки соединения сервера

Далее необходимо в обозревателе найти и развернуть свою БД создать схему и уже в ней создать таблицу. Далее пользуясь кнопкой «+» добавляем два столбца. Поле «По умолчанию» заполнится автоматически после сохранения при выборе серийного типа данных. Также после сохранения серийный тип данных преобразовывается в соответствующий ему тип данных целых чисел.

Окно создания и настройки столбцов таблицы
Окно создания и настройки столбцов таблицы

Повторно открыть это окно можно кликнув правой кнопкой мыши (ПКМ) по таблице в обозревателе.

Так как моим ботом не будет пользоваться большое число пользователей, то я выбрал тип данных с диапазоном от 1 до 32767. Как сказано в официальной документации API ботов Telegram, представленной в пункте «Сервисы для разработки – Telegram» этой статьи:

Unique identifier for this user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier.

Для уникальных идентификаторов пользователей безопасно использование 64-битных целых чисел, которым и является bigint в БД PostgreSQL.

Создадим конструктор __init__модуля, в который будет передаваться идентификатор упрощённого подключения к БД PostgreSQL. Начало создания модуля в файле SQLRequests.py:

Код Python
# адаптер БД PostgreSQL
import psycopg2

class SQLRequests:

  def __init__(self, db_uri):
  '''
    Подключение к БД по идентификатору `db_uri`
    ---------
    Параметры
    ---------
    * `db_uri`: str (строка)
    
      Идентификатор для упрощённого подключения к БД
  '''
      
  # установим соединение с БД c безопасным соединением SSL
  self.connection = psycopg2.connect(db_uri, sslmode='require')
  
  # инициализируем объект обработки строк
  self.cursor = self.connection.cursor()
  
  # включение автоматической фиксации изменений
  self.connection.autocommit = True

Далее необходимо реализовать метод проверки наличия пользователя в БД user_exists и метод добавления пользователя в БД user_add:

Код Python
def user_exists(self, user_id):
  '''
    Проверка в БД на наличие пользователя с id = `user_id`
    ---------
    Параметры
    ---------
    * `user_id`: int (целое число)
  
      Уникальный идентификатор пользователя
  '''
  
  self.cursor.execute(f'SELECT user_id FROM bikbeepbot."UsersBD" WHERE user_id = {user_id}')

  # вернуть значение
  return self.cursor.fetchone()

def user_add(self, user_id):
  '''
    Добавление в БД пользователя с id = `user_id`
    ---------
    Параметры
    ---------
    * `user_id`: int (целое число)

      Уникальный идентификатор пользователя
  '''
    
  self.cursor.execute(f'INSERT INTO bikbeepbot."UsersBD"(user_id) VALUES({user_id})')

Для корректного выполнения запроса название таблицы БД обязательно должно быть в двойных кавычках "TableName". В запросах применяются f-строки появившиеся в Python 3.6. Статья на русском по форматированию строк и с примерами f-строк: f-строки в Python 3.

Для рассылки боту необходимо получить идентификаторы пользователей, для этого реализуем ещё один метод get_users:

Код Python
def get_users(self):
  '''
    Получаем всех пользователей бота
    -------
    Возврат
    -------
    * `users_id`: list[tuples] (список кортежей)

      Список полученных пользователей
  '''

  self.cursor.execute('SELECT * FROM bikbeepbot."UsersBD"')

  # вернуть все значения
  return self.cursor.fetchall()

Хоть разница в методах получения результатов fetchone( ) и fetchall( ) очевидна из названия, но подробнее можно почитать в официальной документации модуля psycopg2, представленной в пункте «Описание используемых пакетов Python – psycopg2» этой статьи.

Подключение и использование созданных модулей

Изначально надо инициализировать созданные модули. Модернизируем файл bot.py:

Код Python
# файл конфигов
from configs import DB_URI
# модуль проверки расписания
from BIKParser import BIKParser as BIKP
# модуль работы с БД
from SQLRequests import SQLRequests as SQLR

...

# инициализируем соединение с БД
db = SQLR(DB_URI)

# инициализируем парсер
parserBIK = BIKP('https://bincol.ru/rasp/prep.php?idprep=000000235')

...

Запоминать идентификаторы пользователей будем при старте общения пользователя с ботов в обработчике команды /start. Для этого в метод welcome добавим проверку на наличие пользователя в БД:

# проверка на наличие пользователя в БД
if (not db.user_exists(message.from_user.id)):
  # добавление пользователя в БД
  db.user_add(message.from_user.id)

Проверять изменение расписания, формировать изображение с расписанием, а затем получать идентификаторы пользователей и отправлять им изображение с расписанием под циклом while True: будем в методе scheduled:

Код Python
# модуль асинхронных возможностей
from asyncio import get_event_loop, sleep

async def scheduled(wait):
  '''
    Проверяем изменение расписания и делаем рассылку через `wait` минут
    ---------
    Параметры
    ---------
    * `wait`: int (целое число)

      Задержка в минутах
  '''

  while True:
    # проверяем изменение расписания
    if (parserBIK.CheckChange()):

      # формируем изображение расписания
      parserBIK.ChangeImage()

      # получаем список пользователей бота
      IdUsers = db.get_users()

      # отправляем всем сообщение
      for user_id in IdUsers:
        await bot.send_photo(user_id[1], open('schedule.png', 'rb'), caption = 'Расписание СильченкоОВ изменено!')
    
    # ожидаем
    await sleep(wait * 60)

# при запуске файла
if (__name__ == '__main__'):
  # запуск задачи
  get_event_loop().create_task(scheduled(15))

  ...

Так как метод get_users() возвращает лист кортежей, то в цикле for необходимо выбирать идентификаторы пользователей, которые в кортеже (0, 1) идут под номером 1. Под номером 0 будет первичный ключ – столбец id, являющийся автоинкрементом.

После запуска бота и начала общения с ботом проверить содержимое столбцов таблицы БД можно в pgAdmin 4, кликнув ПКМ по таблице и выбрав «Просмотр/редактирование данных – Все строки» или выбрав «Запросник» и написав запрос:

SELECT * FROM bikbeepbot."UsersBD"

Нажав кнопку выполнения запроса «▶» (треугольник вправо) внизу отобразится результат запроса.

Результат выполнения запроса
Результат выполнения запроса

Развёртка бота на Heroku

Стек – образ ОС поддерживаемых Heroku. Стеки основаны на дистрибутиве Linux – Ubuntu. Heroku на текущий момент предоставляет два стека, на котором может работает приложение: Heroku-18 и Heroku-20. Цифра в названии зависит от первых цифр версий Ubuntu: Heroku-18 основан на Ubuntu 18.04 и закончит поддержку в апреле 2023 года, а также Heroku-20, поддерживающий Python 3, основан на Ubuntu 20.04 и закончит поддержку в апреле 2025 года. Приложения по умолчанию размещаются на Heroku-20.

Перед развёрткой бота необходимо создать 3 служебных файла в корневой директории, а также выбрать необходимые сборочные пакеты.

Служебные файлы

  1. Файл runtime.txt содержит среду выполнения Python и точное название версии.

python-3.10.0

2. Файл requirements.txt содержит зависимостей для приложения – сторонние пакеты, используемые в исходном коде нашего приложения.

aiogram
requests
bs4
html2image
pillow
psycopg2

3. Файл Procfile содержит команды, выполняемые приложением при запуске. Строки файла должны иметь следующий формат: <process type>: <command>.

worker: python bot.py

Сборочные пакеты

Добавление сборочных пакетов для приложений происходит в разделе «Настройки».

Окно добавления сборочных пакетов
Окно добавления сборочных пакетов
  1. Сборочный пакет для приложений Python heroku/python.

  2. Сборочный пакет для драйвера браузера Chrome https://github.com/heroku/heroku-buildpack-chromedriver.git.

  3. Сборочный пакет для браузера Google Chrome https://github.com/heroku/heroku-buildpack-google-chrome.git. Данный сборочный пакет включает в себя в строке 183 файла /bin/compile флаг --remote-debugging-port=9222 включающий удалённую отладку и запрещающий делать снимок экрана. Для исправления этой ошибки воспользуемся ответвлением этого сборочного пакета от aurelmegn https://github.com/aurelmegn/heroku-buildpack-google-chrome.git.

Выполнение развёртки приложения

Все последующие команды выполняются в командной строке, находясь в корневой директории с исходным кодом бота. Смена директории осуществляется командой cd.

Справка по команде:

Для входа в Heroku CLI выполним команду heroku login, которая откроет окно браузера по умолчанию для выполнения авторизации на Heroku.

Окно авторизации в браузере, в котором уже выполнен вход в Heroku
Окно авторизации в браузере, в котором уже выполнен вход в Heroku

Справка по командам Git представлена в пункте «Сервисы для разработки – Git» этой статьи.

Создадим Git-репозиторий командой git init. Выполняется один раз при первой развёртки приложения.

Для отслеживания новых, изменённых и удалённых файлов в текущей директории используется команда git add ..

Для фиксации изменений используется команда git commit -m "First release". Для повторных фиксаций изменений правильнее менять сообщение в кавычках.

Просмотр адресов удалённых репозиториев может осуществиться с помощью команды git remote -v. Команда не является обязательной.

Для отправки изменений в удалённый репозиторий выполним команду git push heroku master.


Результат работы

Для просмотра состояния приложений Heroku можно воспользоваться командой heroku ps.

Запустим приложение, выполнив команду heroku ps:scale worker=1.

Пример работы бота
Пример работы бота
Пример работы бота
Пример работы бота

Для остановки приложения можно выполнить команду heroku ps:scale worker=0 -a bikbeepbot.

Чтобы получить исходное содержимое репозитория приложения можно осуществить клонирование репозитория по пути из которого выполнится команда heroku git:clone -a bikbeepbot.


Ошибки

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

Журнал приложения

app[worker.1]: [1218/191809.376330:ERROR:bus.cc(393)] Failed to connect to the bus: Failed to connect to socket /var/run/dbus/system_bus_socket: No such file or directory

app[worker.1]: [1218/191809.480389:ERROR:sandbox_linux.cc(376)] InitializeSandbox() called with multiple threads in process gpu-process.

app[worker.1]: /app/bot.py:67: DeprecationWarning: There is no current event loop
app[worker.1]:   get_event_loop().create_task(scheduled(15))

Последующие ошибки (от 11 марта 2022)

Вследствие последних политических событий ОГАПОУ «Белгородский индустриальный колледж» ограничил доступ к своему сайту странам Европы, где и размещается бот. Для решения данной проблемы было принято решение воспользоваться сервисом онлайн-просмотра кода файлов по URL от Дмитрия Елисеева.

После небольших правок в методе проверки расписания CheckChange бот продолжил свою работу. Во всей статье код метода CheckChange представлен без этих правок.


Исходный код

Файл configs.py:

# токен бота из @BotFather
TOKEN = '2118918752:token'

# идентификатор упрощённого подключения к БД PostgreSQL
DB_URI = 'postgres://user:password@host:port/database'

Файл bot.py:

Код Python
# файл конфигов
from configs import TOKEN, DB_URI
# модуль проверки расписания
from BIKParser import BIKParser as BIKP
# модуль работы с БД
from SQLRequests import SQLRequests as SQLR

# фреймворк для Telegram Bot API
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
# модуль асинхронных возможностей
from asyncio import get_event_loop, sleep

# присвоение токена
bot = Bot(token=TOKEN)

# инициализируем обработчик входящих обновлений
dp = Dispatcher(bot)

# инициализируем соединение с БД
db = SQLR(DB_URI)

# инициализируем парсер
parserBIK = BIKP('https://bincol.ru/rasp/prep.php?idprep=000000235')

@dp.message_handler(commands=['start'])
async def welcome(message: types.Message):
  await message.answer('При изменении расписания СильченкоОВ будут присылаться уведомления.')

  # проверка на наличие пользователя в БД
  if (not db.user_exists(message.from_user.id)):
    # добавление пользователя в БД
    db.user_add(message.from_user.id)

async def scheduled(wait):
  '''
    Проверяем изменение расписания и делаем рассылку через `wait` минут
    ---------
    Параметры
    ---------
    * `wait`: int (целое число)

      Задержка в минутах
  '''
      
  while True:
    # проверяем изменение расписания
    if (parserBIK.CheckChange()):
      # формируем изображение расписания
      parserBIK.ChangeImage()

      # получаем список пользователей бота
      IdUsers = db.get_users()

      # отправляем всем сообщение
      for user_id in IdUsers:
        await bot.send_photo(user_id[1], open('schedule.png', 'rb'), caption = 'Расписание СильченкоОВ изменено!')

    # ожидаем
    await sleep(wait * 60)

# при запуске файла
if (__name__ == '__main__'):
  # запуск задачи
  get_event_loop().create_task(scheduled(15))

  # запуск режима длительного опроса
  executor.start_polling(dp, skip_updates=True)

Файл BIKParser.py:

Код Python
# модуль запросов
from requests import get
# модуль парсера
from bs4 import BeautifulSoup as BS
# модуль для работы с файлами
from os.path import exists
# модуль создания картинки из html
from html2image import Html2Image as HTI
# модуль работы с изображениями
from PIL import Image

class BIKParser:

  # конструктор
  def __init__(self, url):
    '''
      Парсер сайта bincol.ru страницы `url`
      ---------
      Параметры
      ---------
      * `url`: str (строка)
    
        Адрес страницы с расписанием
    '''

    self.url = url

  def CheckChange(self):
    '''
      Проверка изменения расписания
      -------
      Возврат
      -------
      * `ResultCheck`: bool (логическая переменная)
        
        Результат проверки
    '''
  
    # парсинг страницы расписания
    h = get(self.url)
    html = BS(h.content, 'html.parser')
  
    # получим все строки таблиц
    new_schedule_buf = html.find_all('tr')
  
    # сформируем html код расписания
    self.new_schedule = ['<table><tbody>']
    for num in range(1, len(new_schedule_buf)):
      bufStr = str(new_schedule_buf[num])
      # заберём только нужные данные
      if (('Понедельник' in bufStr) or ('Вторник' in bufStr) or ('Среда' in bufStr) or ('Четверг' in bufStr) or ('Пятница' in bufStr) or ('Суббота' in bufStr) or ('Воскресенье' in bufStr) or ('<td valign="top">' in bufStr)):
        self.new_schedule.append(bufStr)
    self.new_schedule.append('</tbody></table>')
  
    # переменная результата проверки
    ResultCheck = False
  
    # проверка на наличие файла старого расписания
    if (exists('old_schedule.txt')):
      # откроем старое расписание
      old_schedule = open('old_schedule.txt', 'r').read()
    
      # если расписания отличаются
      if (str(self.new_schedule) != old_schedule):
        # сохраним новое расписание
        self.FillFileSchedule()
  
        # изменим результат проверки
        ResultCheck = True
    else:
      # создадим и заполним файл старого расписания
      self.FillFileSchedule()
  
      # изменим результат проверки
      ResultCheck = True
  
    # вернём результат проверки
    return ResultCheck

  def FillFileSchedule(self):
    '''Заполнение файла расписания'''
  
    # заполнить файл старого расписания
    f = open('old_schedule.txt', 'w')
    f.write(str(self.new_schedule))
    f.close()

  def ChangeImage(self):
    '''Формируем изображение расписания'''
  
    # инициализируем метод создания изображения
    hti = HTI(output_path='/app')
  
    # получим изображение из html страницы
    hti.screenshot(html_str=''.join(self.new_schedule), save_as='schedule.png')
  
    # обрежем пустоту у изображения
    old_image = Image.open('schedule.png')
    new_image = old_image.crop(old_image.getbbox())
    new_image.save('schedule.png')

Файл SQLRequests.py:

Код Python
# адаптер БД PostgreSQL
import psycopg2

class SQLRequests:

  def __init__(self, db_uri):
    '''
      Подключение к БД по идентификатору `db_uri`
      ---------
      Параметры
      ---------
      * `db_uri`: str (строка)
    
        Идентификатор для упрощённого подключения к БД
    '''
    
    # установим соединение с БД c безопасным соединением SSL
    self.connection = psycopg2.connect(db_uri, sslmode='require')
    
    # инициализируем объект обработки строк
    self.cursor = self.connection.cursor()
    
    # включение автоматической фиксации изменений
    self.connection.autocommit = True
  
  def get_users(self):
    '''
      Получаем всех пользователей бота
      -------
      Возврат
      -------
      * `users_id`: list[tuples] (список кортежей)
    
        Список полученных пользователей
    '''
    
    self.cursor.execute('SELECT * FROM bikbeepbot."UsersBD"')
    
    # вернуть все значения
    return self.cursor.fetchall()

  def user_exists(self, user_id):
    '''
      Проверка в БД на наличие пользователя с id = `user_id`
      ---------
      Параметры
      ---------
      * `user_id`: int (целое число)
      
        Уникальный идентификатор пользователя
    '''
    
    self.cursor.execute(f'SELECT user_id FROM bikbeepbot."UsersBD" WHERE user_id = {user_id}')
    
    # вернуть значение
    return self.cursor.fetchone()

  def user_add(self, user_id):
    '''
      Добавление в БД пользователя с id = `user_id`
      ---------
      Параметры
      ---------
      * `user_id`: int (целое число)
      
        Уникальный идентификатор пользователя
    '''

    self.cursor.execute(f'INSERT INTO bikbeepbot."UsersBD"(user_id) VALUES({user_id})')

Файл runtime.txt:

python-3.10.0

Файл requirements.txt:

aiogram
requests
bs4
html2image
pillow
psycopg2

Файл Procfile:

worker: python bot.py

Заключение

Бот стабильно работает уже с 7 декабря без каких-либо изменений.

Повторюсь, что данная статья не несёт никаких грандиозных нововведений и предложений, а лишь освещает мой путь по создания бота в Telegram. В статье получилось очень много теории и документации, но ведь именно в них и заключается суть успешной разработки ПО.

По поводу допущенных мной ошибок или предложений по исправлению ошибок из пункта «Ошибки – Журнал приложения» этой статьи, а также по вопросам можно писать в комментарии под этой статьёй, в личные сообщения (ЛС) нашей группы ВКонтакте или на нашу почту:

fas.offical@ya.ru

Tags:
Hubs:
Total votes 5: ↑3 and ↓2+1
Comments4

Articles