Pull to refresh

Telegram-bot: моя история. Часть вторая

Reading time 9 min
Views 38K

Доброго времени суток, Хабрахабр! Данный материал является продолжением первой части, в которой освещены инструменты и возможности разработки продукта на облачной платформе. Примером является актуальное мобильное расширение доступа к расписанию пар в университете — Telegram-bot.

Уделив время первой публикации, вы можете ознакомиться не только с первоначальным развертыванием и необходимыми ресурсами, но и с обзором приложения от автора. Информация ниже сконцентрирована в большей степени на описании технической стороны и в меньшей на выводах по итогам на текущий момент.

UPD: проект успешно прошел рефакторинг с помощью авторского фреймворка для расписаний занятий университетов — «Rutetider» (статья на Хабрахабре, GitHub). Поэтому некоторые моменты в этой статье могут не совпадать, часть когда может отсутвовать или выглядеть по-другому, но концепция сохранена.

Технический обзор проекта


Параллельно с объяснениями вы можете рассчитывать на скриншоты «с мест событий» для более глубокого понимания процесса написания, также рекомендуется обращать внимание на полный исходный код (github.com) и при достаточном интересе — вникать в предоставляемые ссылки по причине отсутствия описания тех или иных, на усмотрение автора, пояснений используемых и предоставленых компонентов. Если часть описания не понятна, вопрос можно задать автору в комментариях или диалогах, или уделить две минуты на взаимодействие с предметной областью напрямую, что позволит лучше ориентироваться в статье и приведенных примерах.

Расписание пар университета


На момент проектирования архитектуры приложения не было других вариантов получения расписания лекций, кроме прямого сбора информации с официального сайта (парсинг). API для платформы, на котором располагается расписание, в наличии нет, а возможность извлечь информацию непосредственно из базы данных отсутствует.

Необходимая техническая операция (github.com) тесно связана с полезными запланированными работами на сервере, сведения о которых расположены в конце раздела.

Используемыми модулями являются:

  • Beautiful Soup — работа со структурой данных разметки HTML;
  • Requests — отправка HTTP-запросов.

Файл dataLib.py содержит массив ключей-значений для осуществления запроса на каждую академическую группу и функцию для внесения временного диапазона dateToDateForm(), чтобы определять рамки дат, для которых необходимо расписание.

groupExample = {'TimeTableForm[date1]': firstDay, 'TimeTableForm[date2]': lastDay,
'TimeTableForm[faculty]':5, 'TimeTableForm[course]':3, 'TimeTableForm[group]':613}

Далее следует отправка post-запроса для получения объекта страницы и дальнейшая передача в объект soup, имеющий ряд методов для удобной работы.

for formData in dataLib.dateToDateForm(firstDay, lastDay):
r = requests.post(config.url, data = formData)
	soup = BeautifulSoup(r.text, 'html.parser')
	dataProcessing.dataProcessing(soup)

Модуль dataProcessing имеет одноименную функцию, в которой обрабатываются необходимые данные. Например, переменная mainHtmlTable — это базовая таблица с расписанием, имеющая свой уникальный идентификатор.

mainHtmlTable = soup.find("table", {"id": "timeTableGroup"})


(часть таблицы для наглядного примера, полную версию можно «пощупать» по ссылке, предварительно выбрав факультет, курс и группу)

Еще один пример — даты, основанный на получении всех элементов блочного типа, имеющих внутри себя текст длиной равной десяти («12.12.2016» — десять символов).

divDate = [i.text[:-5] for i in mainHtmlTable.findAll('div') if len(i.get_text()) == 10][:-2]

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

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

Общая концепция взаимодействия с пользователем


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

Проделанная работа не стала исключением, несмотря на обратное в исходной точке продукта, и по итогам сформировался некий концептуальный шаблон проектирования для большинства операций, включающий в себя обработчик запросов от пользователя (Handler), выполнение ряда последующих операций (Query) и генерацию исходных данных (Reply) в ответ.


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

Пример обработки запросов


В последующем участке основного файла-обработчика (github.com) всех запросов можно наблюдать post-запрос от пользователя, совершившего выбор одного из пунктов меню с названием «Обновления» (данное текстовое значение доступно как атрибут объекта сообщений «message» по причине дублирования выбора меню в чат), внесение данных о пользователе (имя, фамилия, идентификатор чата, текстовое значение «news» как выбор пункта меню и никнейм) с помощью метода otherFeature() класса middlewareUserData (github.com). Данная функция записывает действия пользователей для статистики использования функций приложения. После предыдущих обработок отсылается как ответ по привязанному идентификатору чата текстовое сообщение с информацией о месторасположении новостей проекта.

@bot.message_handler(func=lambda mess: "Обновления" == mess.text, content_types=['text'])
def handle_text(message):

    middlewareUserData.otherFeature(message.chat.first_name, message.chat.last_name, message.chat.id, 'news', message.chat.username)
    bot.send_message(message.chat.id, 'Обновления и дополнительную информацию можно посмотреть тут — https://telegram.me/dutbotupdates')

Иллюстрация процесса приведена ниже. Картинки, связанные с таблицами баз данных, кликабельны (можно рассмотреть лучше), потому что представлены в плохом качестве, но именно так, чтобы не терять целостность картины разработки.



Концепция получения расписания


Расписание на сегодняшний или завтрашний день доступно двумя способами. Рассмотрим первый вариант — через постепенный ввод данных: необходимо выбрать факультет, курс и группу. Напомню, что атрибут «text» объекта «message» — это текстовое значения выбора пункта меню.

1. Кнопка «Получить расписание» — пользователь присылает базовый набор данных:

middlewareUserData.getUser(message.chat.first_name, message.chat.last_name, message.chat.id, message.chat.username)

Следовательно, действие отображается в таблице базы данных в таком формате (последующие примеры работы базы данных в конце раздела):


2. Выбор факультета:

middlewareUserData.updateFaculty(message.text, str(message.chat.id))

3. Выбор курса и группы, скрипт которого несколько отличается от предыдущих, потому что отображение списка группа на экране происходит после запроса к таблице с полным расписанием, где изымается весь список групп на заданный пользователем факультет и курс:

middlewareUserData.updateCourse(message.text[:1], str(message.chat.id))

fucAndCourse = middlewareUserData.getFacultyAndGroup(str(message.chat.id))

groupList = selectData.selectGroup(fucAndCourse[0], fucAndCourse[1])

basicMarkupRows.markRowGroupList(groupList, message)

4. Кнопка «На сегодня» или «На завтра», код которой содержит актуальный набор дат (сегодня, завтра, индекс сегодняшнего дня), проверку ввода от пользователя (база данных одна для всех, необходимо взять результат определенного пользователя) и само отображение расписания (github.com):

day = selectData.selectDates()

lastGroup = middlewareUserData.summaryVerification(str(message.chat.id))

bot.send_message(message.chat.id, "Расписание на сегодня ({0}):".format(day[1]))
message.text = printController.show(lastGroup, day[1])
bot.send_message(message.chat.id, message.text)

Иллюстрация поочередного заполнения таблицы к действиям пользователя:


Вернуться назад в предыдущее меню


Если вы внимательно читали предыдущий раздел, то обязательно обратили бы внимание на самый важный элемент таблицы — «empty». Функция возвращения назад отталкивается непосредственно от количества таких записей:

«Получить расписание» — формируется первоочередный объект вместе с четырьмя пустыми значениями (текстовое значение «empty»), потому что не выбран факультет, курс, группа и пункт финального меню (например, расписание на определенный день или подписка на группу);

Выбор факультета — уже на один «empty» меньше, выбор курса — уже на два.

Скрипт (github.com) определяет количество таких полей в строке таблицы для пользователя, зная заранее, к какому количеству какой метод из необходимых вызывать. Например, мы остановились на выборе курса, значит, мы совершили выбор факультета и у нас осталось три «пустых» значения:

if emptyCount == 3:
	backButton.cancelOperation(str(message.chat.id), 3)
	basicMarkupRows.markRowGetFacultyList(message)

Метод cancelOperation (github.com) определяет функцию, которая занимается очисткой поля, которое мы хотим отменить:

if self.emptyCount == 3:
	self.cancelFaculty(self.chatid)

Если вы ошиблись с факультетом, его нужно поменять. Под капотом ситуация следующая — по идентификатору пользователя меняем значение факультета (например, «Информационные технологии») на пустое значение («empty») через простой SQL-запрос «Update»:

def cancelFaculty(self, chatid):
	self.chatid = chatid
self.cursor.execute("UPDATE statistic SET faculty = (%s) WHERE id IN (SELECT max(id) FROM statistic WHERE chatid = (%s))", ('empty', self.chatid))
	self.connection.commit()

Достаточно изменить данные в таблице статистики, взять поле «факультет», где идентификатор поля таблицы является максимальным для определенного идентификатора пользователя (то есть выбор самого последней строчки таблицы для нужного пользователя — это текущее состояние меню и данных).

Подписка на расписание группы



Функция оформления доступа к расписанию группы на два дня с первого меню основана на одном внесении своей группы в таблицу, то есть необходимо выбрать желаемую группу и подтвердить подписку, которая доступна в самом последнем меню (github.com).

if message.text == "Подписаться на эту группу":
	middlewareUserData.subscribe(message.chat.first_name, message.chat.last_name, message.chat.id, lastGroup, message.chat.username)

message.text = 'Вы подписались на группу {0}.'.format(lastGroup)
	bot.send_message(message.chat.id, message.text)

Функция subscribe() (github.com) вставляет в отдельную, не связанную с остальными таблицу хранения подписок знакомые нам данные, но самыми главными являются идентификатор и сама группа.

def subscribe(self, firstname, lastname, chatid, group, username):
	* селф-переменные*
	self.cursor.execute("INSERT INTO subscribers 
(firstname, lastname, chatid, groupa, username) VALUES (%s, %s, %s, %s, %s)", (self.firstname, self.lastname, self.chatid, self.group, self.username))
		self.connection.commit()

Запланированные работы на сервере


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

Мой бот нуждается в ежедневном обновлении расписания пар и сегодняшних и завтрашних дат, которые успешно доступны через функцию в коде ниже. Так как сервер расположен в другом часовом поясе, мне необходимо отнять от нужного времени два часа (обновления у меня происходят в 00:01). С понедельника по пятницу изымается информация на текущую неделю, в воскресенье (индекс дня равен пяти) — на следующую.

@sched.scheduled_job('cron', day_of_week='mon-sun', hour=22, minute=1)
@sched.scheduled_job('cron', day_of_week='mon-fri', hour=22, minute=1)
@sched.scheduled_job('cron', day_of_week=5, hour=22, minute=1)

Исходный код (github.com) не является реально действующим в боевых условиях и пока дорабатывается, но концепция определена верно (на 15.12 исправлено).

Работа с базами данных


Платформа Heroku позволяет непосредственно работать с базами данных из родной консоли (Linux Ubuntu 16.04) или же получать доступ через SQL Shell (Windows 7), предварительно вводя данные по типу паролей и логинов.

Серверная часть бота, связанная с базами данных, ограничена простыми SQL-запросами (например, select, insert, update), а необходимость создания и удаления таблиц соответствующими командами create и drop.

Выводы



Набирая программный код на коленке, я даже не предполагал, что мои ожидания насчет итогового эффекта не оправдаются, к счастью — в лучшую сторону. Но никто из в той или иной мере причастных к приложению не думает, что текущее положение дел не заслужено. Занимаясь модернизацией архитектуры, я ни разу не планировал использовать базы данных или вводить дополнительные функции. Но в итоге все это присутствовало в плане разработки и удалось не только довести проект до серьезного отрезка, но и получить необходимый качественный результат как в общем, так и в личном плане. На каком-то этапе пришлось «отказаться» от привычной модели поведения, так как было необходимо вживаться в разные роли по ходу жизненного цикла разработки программного обеспечения. В процессе работы даже удалось заняться социальным маркетингом, распространяя информацию о разработанном продукте среди студентов, «высасывая», как тогда казалось, хоть какой-нибудь онлайн.

Даже такой незначительный результат — более 100 уникальных пользователей ежедневно — не только положительно сказывается на отношении к автору ресурса, каждый день решающего проблемы окружающих, но и является бесспорным мотиватором продолжать заниматься улучшениями процессов во всех сферах, касающихся предметной области.
Tags:
Hubs:
+12
Comments 1
Comments Comments 1

Articles