Содержание
Постановка задачи и первичные варианты решения
В связи с ежедневными вечерними (да ещё и постоянно в разное время) обновлениями расписания в ОГАПОУ «Белгородский индустриальный колледж» необходимо программное обеспечение (ПО), которое будет следить за расписанием и уведомлять при его изменении.
Среди вариантов решения были рассмотрены:
собственное андроид-приложение;
бот/сервис в социальной сети/мессенджере.
В начале разработки приложения под андроид в 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:
выполнив в командной строке команду
heroku help
;
Для удобства дальнейшей работы необходимо проверить, чтобы в системной переменной 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 и запросах:
документы RFC2068 и RFC2616, а также RFC2068 (на русском);
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 – развертка в различных географических регионах.
Классическая структура бота 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
это ссылка на конкретный экземпляр класса. Для обращения к переменным экземпляра всегда нужно дописывать self
: self.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 ofbrowser.flags
:
В которой говориться об отмене флагов по умолчанию при использовании пользовательских флагов или изменении значений флагов браузера. В таких случаях нужно будет не забыть вернуть флаги по умолчанию: --default-background-color=0
(объяснение флага) и --hide-scrollbars
(объяснение флага).
Создание модуля по работе с БД PostgreSQL
В ресурсы приложения на Heroku необходимо добавить дополнение Heroku Postgres для добавления БД к приложению. При добавлении выбираем бесплатный план Hobby Dev.
Добавим в файл конфигов 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 служебных файла в корневой директории, а также выбрать необходимые сборочные пакеты.
Служебные файлы
Файл
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
Сборочные пакеты
Добавление сборочных пакетов для приложений происходит в разделе «Настройки».
Сборочный пакет для приложений Python
heroku/python
.Сборочный пакет для драйвера браузера Chrome
https://github.com/heroku/heroku-buildpack-chromedriver.git
.Сборочный пакет для браузера Google Chrome
https://github.com/heroku/heroku-buildpack-google-chrome.git
. Данный сборочный пакет включает в себя в строке 183 файла/bin/compile
флаг--remote-debugging-port=9222
включающий удалённую отладку и запрещающий делать снимок экрана. Для исправления этой ошибки воспользуемся ответвлением этого сборочного пакета от aurelmegnhttps://github.com/aurelmegn/heroku-buildpack-google-chrome.git
.
Выполнение развёртки приложения
Все последующие команды выполняются в командной строке, находясь в корневой директории с исходным кодом бота. Смена директории осуществляется командой cd
.
Справка по команде:
выполнив команду
cd /?
;
Для входа в Heroku CLI выполним команду heroku login
, которая откроет окно браузера по умолчанию для выполнения авторизации на 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
.
Разработанный бот в Telegram – @BIKbeep_bot.
Для остановки приложения можно выполнить команду 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
Комментарии (4)
AMDmi3
25.03.2022 23:21+2Вы используете асинхронный aiogram, но синхронные requests и psycopg2. На ваших нагрузках это скорее всего не заметно, но в общем случае так делать не стоит, потому что вы получаете по сути синхронное приложение выдающее очень низкий rps и блокирующее всё общение с пользователями на время долгого запроса в http или базу. Для асинхронной работы есть aiohttp и aiopg/asyncpg.
Cepera_C_A Автор
26.03.2022 12:57Спасибо, обязательно познакомлюсь с данными библиотеками и начну их применять в будущих проектах.
Да, для текущего бота это совсем не критично, но для ботов, в которых присутствует много общения с пользователем, согласен – это очень важно. По факту этот бот писался для единственного пользователя (не меня), и в боте не реализовано никакого общения, кроме команды /start, хотя я и старался сделать структуру в приложении с возможностью добавления других преподавателей/групп с минимальным изменением в коде. Собственно, ради этого я и добавил БД.
orrollo
на картинке "Разница между растровой и векторной графикой в структуре" растр и вектор перепутаны местами. Ну и - зачем так много букв? Разница растра и вектора в статье про телеграм-бот?
Cepera_C_A Автор
Что касается ошибки, то полностью признаю – глупая ошибка. Уже исправил. Спасибо.
Одновременно отвечая на оба Ваших вопроса, хочу напомнить, что это не классическая пошаговая статья по созданию бота для Telegram вида: «Скопируйте код, нажмите сюда, и всё заработает». В этой статье я рассказал про мой путь по созданию конкретного бота, который работает с растровыми изображениями, используя библиотеку Pillow. Именно из-за работы бота с изображениями я и прикрепил картинки со сравнением двух видов компьютерной графики. В данной статье я также приложил теоретические сведения и документацию по всем используемым программным продуктам и библиотекам Python, а также рассказал о всех проблемах, с которыми пришлось столкнуться. О всём этом я говорил в разделе «Заключение» этой статьи: