Такие сообщения мне приходят каждое рабочее утро
Такие сообщения мне приходят каждое рабочее утро

Всем привет! Зовут меня Виталий, автор тг канала Детектив данных про мой вкат в аналитику данных.

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

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

Эта статья будет полезна как начинающим в мире Python, так и продвинутым пользователям, которые проходили мимо работы с телеграммом

Немного предыстории: есть у меня один регулярный ежедневный отчёт, который грузится самым первым, еще до начала рабочего дня. Отчёт состоит из нескольких тяжелых SQL запросов, и по ним я обычно с помощью команды %%time отслеживал скорость загрузки данных. В запросы могут быть внесены изменения, и хочется отследить повлияло ли это на быстродействие выгрузки. И вообще понять - а как сегодня работает сервер, будем ли мы летать или пол дня выгружать "select * from table".

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

Ты можешь быть на созвоне, есть, спать в конце концов - а тут сообщение на часах "отчёт Х загружен". Отлично, сейчас поставлю следующую выгрузку а с этими данными можно начинать работать.

Наконец-то мне пригодилась функция скриншота экрана часов)
Наконец-то мне пригодилась функция скриншота экрана часов)

Или что поинтереснее "запрос Y выполнен за пятнадцать минут" Хм... А так то он обычно выгружается по сорок - значит пора бить тревогу и идти смотреть - а что там произошло? а что-то да произошло раз время сократилось. А ты вовремя пришёл, поправил и поставил заново - не потеряв драгоценные часы работы, и поиск места ошибок постфактум окончания выгрузки с неполными данными.

Порядок статьи такой:

  • создаём своего бота, берём с него всю инфу

  • пишем код на отправку сообщений

  • нюансы и доделки

  • график

Создаём бота в Телеграме

Открываем поиск и ищем 

@BotFather

Придумываем название бота (должно оканчиваться на bot), пусть будет detective_test_report_bot

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

1234567894:HUJRhjh_sPoO135eQz4EwbFBKJkTcIGBMCM66

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

/setuserpic

Переходим в уже наш созданный бот и жмём/вводим команду

/start

И обязательно пишем боту любой текст

hello world

С телегой закончили, начинаем питонить:

import requests
TOKEN = "В КАВЫЧКИ ВСТАВЛЯЕМ СВОЙ ТОКЕН"
url = f"https://api.telegram.org/bot{TOKEN}/getUpdates"
print(requests.get(url).json())

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

…'chat': {'id'4815162342,…. Вот  это число id нам и нужно это наш чат айди

 Сохраняем ваш чат айди в переменную

Готово. Уже сейчас мы можем отправить себе первое сообщение: 

import requests
report_name = 'Отчёт'
bot_token = '1234567894:HUJRhjh_sPoO135eQz4EwbFBKJkTcIGBMCM66'
chat_id = '4815162342'

send_message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
message_text = f'Отчёт {report_name} - загрузка начата'
 payload = {
   'chat_id': chat_id,
   'text': message_text
   }
response = requests.post(send_message_url, data=payload)
message_text

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

Не может быть
Не может быть

Давайте теперь подумаем как мы можем улучшить и сделать сообщения более полезными и содержательными

  • Время загрузки отчетов

  • Небольшое условное форматирование, смайлики

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

  • Графики

Поехали сначала - время запроса

Перед запросом, или отчётом фиксируем текущее время

import time
start_time = time.time() 

после отчета или его части фиксируем время окончания, и округляем получившуюся разницу :

end_time = time.time()
elapsed_time = end_time - start_time
elapsed_minutes_end = round(elapsed_time / 60)

Теперь мы можем добавить в сообщение время выполнения запроса, или загрузки отчета в целом. Просто редактируем наш message_text заодно выполнив два переноса строки с помощью «\n\n» не забыв взять сообщение в скобки

message_text = (f'Запрос 3 - данные загружены за {elapsed_minutes_3} минут\n\n'
f'Отчёт {report_name} загружен за {elapsed_minutes_end} минут ')

Супер. Отчет стал полезнее, давайте поколдуем что-бы он стал немного нагляднее:

Добавим новый атбирут в payload:

'parse_mode': 'Markdown' – теперь мы можем просто поставить ** в тексте и шрифт выделится жирным, ну или

'parse_mode': 'HTML' если вы как и я (используем свои знаниями из 2005 года) хотите использовать теги <b> </b>

Что-то не хватает – смайликов! Помогут отделить один вид сообщений от других – как ни странно ничего выдумывать не надо, просто копируете понравившийся смайлик хоть тут в телеге и вставляете в свой текст в питоне. Можно вставить напрямую, а можно использовать юникод код например «\u2198\ufe0f» для стрелки (юникод можно посмотреть в теле ссылки например telegram.org/a/img-apple-64/2198-fe0f.png или найти любую таблицу с кодами)

Может случиться необходимость когда сообщение с бота нужно отправить нескольким коллегам – всё просто отправляем коллегу писать сообщение бота и смотрим его чат айди, с помощью первого кода в статье, а дальше запускаем самый стандартный цикл отправки через for, теперь оба айди сохраняем в переменную

chat_ids = ['151675936', '463459322']

и сам цикл

for chat_id in chat_ids:
   payload = {'chat_id': chat_id,  'text': message_text, 'parse_mode': 'Markdown' }
   response = requests.post(send_message_url, data=payload)
message_text -- в конце (уже вне цикла) я вывожу себе одно сообщение для проверки.

Ну вот и всё, ничего сложного и довольно удобно. Но отправлять можно не только сообщения, но и небольшие файлы и фото, например графики (только аккуратно, данные составляющие коммерческую тайну через телегу я бы не советовал никуда отправлять без предварительных согласований), а что-то нейтральное - вроде времени отработки запроса - почему бы и нет? В следующем посте - создадим мини базу для хранения времени загрузки отчётов, настроим её обновление, отрисуем график с помощью библиотеки matplotlib, покопаемся в мелочах и отправим себе удобную картинку с анализом нашей загрузки. Жду ваших эмоций к посту, комментариев - и до скорой встречи в новых статьях!

Ну а теперь самое интересное - отрисуем график загрузки

? Отчёт "Daily report" загружен за 87 минут! Среднее время загрузки составило 83 минуты
? Отчёт "Daily report" загружен за 87 минут! Среднее время загрузки составило 83 минуты

Давайте договоримся о переменных. Почти все они уже были определены в прошлый раз, и сейчас мы их просто обозначим для независимости первой части стати от второй статей друг от друга.

  • наименование отчёта - это нужно для фильтрации в дальнейшем

  • токен ТГ бота

  • наш чат айди с ботом

  • и время загрузки отчета

report_name = 'Daily report'
bot_token = '1234567894:HUJRhjh_sPoO135eQz4EwbFBKJkTcIGBMCM66'
chat_id = '4815162342'
last_value= 87

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

import pandas as pd
data = {
 'дата': ["04.06", "05.06", "07.06", "08.06", "09.06", "10.06", "11.06", "12.06", "13.06", "14.06", "15.06", "16.06"],
 'значение': [74, 80,  77,  86,  103,  70,  93,  81,  81,  71,  80,  78],
 'отчёт': ["Daily report"] * 12 #
}
df = pd.DataFrame(data)
df.to_csv('data.csv', index=False)

Далее создаём таблицу с сегодняшними значениями и записываем её в csv, не переписывая а добавляя его в базу (mode='a')

from datetime import datetime
data = {
  'дата': [datetime.now().strftime('%-d.%m')],
  'значение': [last_value],
  'отчёт': [report_name]
}
df_time = pd.DataFrame(data)
df_time.to_csv('data.csv', mode='a', header=False, index=False)

Затем читаем наш файл, фильтруем по текущему отчёту и выбираем последние 10 записей

loaded_df = pd.read_csv(csv_file)
filtered_df = loaded_df[loaded_df['отчёт'] == report_name].tail(10)
filtered_df

Далее минутка или даже ячейка душноты - мини функция которая определяет правильную форму слова "Минута" в зависимости от числа. Да, вроде и не неважно, но кто я чтобы сопротивляется своему внутреннему перфекционизму?

def get_minute_word(number):
    if 11 <= number % 100 <= 19:
        return 'минут'
    else:
        last_digit = number % 10
        if last_digit == 1:
            return 'минуту'
        elif 2 <= last_digit <= 4:
            return 'минуты'
        else:
            return 'минут'

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

  • Оси,

  • Среднее время загрузки,

  • Минимальное и максимальное значение,

  • Последнее (текущее) значение времени загрузки - перезапишем last_value (хоть можно и оставить, но для надёжности, я всё-таки решил взять последнее значение из таблицы)

  • Правильные формы "Минуты" для последнего и среднего значения

import matplotlib.pyplot as plt
import requests
from io import BytesIO
# Подготовка данных для графика
x_data = filtered_df['дата'].astype(str).tolist()
y_data = filtered_df['значение'].tolist()
average_value = round(sum(y_data) / len(y_data))
min_value = min(y_data)
max_value = max(y_data)
last_value = y_data[-1] 
minute_last = get_minute_word(last_value)
minute_avg = get_minute_word(average_value)

Ну и сам график (важно поместить весь код ниже - в одну ячейку)

# Параметры графика
# Размер графика, основная линия, линяя среднего + легенда, в которой, мы сразу укажем среднее время загрузки графика и заголовок
plt.figure(figsize=(12, 6))
plt.plot(x_data, y_data, linestyle='-', linewidth=3, color='teal')
plt.axhline(y=average_value, color='red', linestyle='--', linewidth=2, label=f'В среднем: {average_value} {minute_avg}')
plt.title(f'Cкорость загрузки отчёта "{report_name}" за последние десять дней, мин.', fontsize=15, fontweight='bold')

# Далее отображаем на графике только значения минимума, максимума и последнего времени загрузки. 
# Учитываем среднее время, для понимания - будет ли последнее значение выше или ниже относительно своей точки графика. 
# Добавляем цвет (сейчас  интуитивно непонятно что лучше - высокое или низкое значение) - пусть минимальное время загрузки будет зеленым, и наоборот самая долгая загрузка (и выше средней последнее значение) - будут красными.
for i, (x, y) in enumerate(zip(x_data, y_data)):
  if y == min_value:
    plt.text(x, y - 3, f'{y}', ha='center', fontsize=15, color='green') # Мин значение снизу
  elif y == max_value:
    plt.text(x, y + 2, f'{y}', ha='center', fontsize=15, color='red')  # Макс значение сверху
# Выводим последнее значение только для последней даты
  elif i == len(y_data) - 1:
    if y < average_value:
      plt.text(x, y - 3, f'{y}', ha='center', fontsize=15, color='green') # Last value снизу и зеленым
    else:
      plt.text(x, y + 2, f'{y}', ha='center', fontsize=15, color='red')  # Last value сверху и красным

# Настройка осей и легенды. Подписи на временной оси X оставим, а Y будто и не нужна, ведь есть значения. 
# Определяем мин и макс оси Y - мы строим не от нуля, нам важно видеть мелкие колебания - опытным путем на этих данных оптимально +-10%
plt.xticks(x_data)
plt.yticks([])
plt.ylim(min(y_data)  0.9, max(y_data)  1.1)

# убираем рамку вокруг графика
for spine in plt.gca().spines.values():
  spine.set_visible(False)

# показываем легенду
plt.legend(frameon=False, fontsize=12)

# Сохранение в буфере и отправка графика. Будто и нет смысла сохранять график в виде картинки в памяти, так как всё равно этот график придёт вам в ТГ.
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)

send_photo_url = f'https://api.telegram.org/bot{bot_token}/sendPhoto'
files = {'photo': buf}
payload = {
    'chat_id': chat_id,
    'caption': f'? Отчёт "{report_name}" загружен за {last_value} {minute_last}!\nСреднее время загрузки составило {average_value} {minute_avg}',
    'parse_mode': 'Markdown'}
response = requests.post(send_photo_url, files=files, data=payload)

buf.close()

Результат еще раз:

? Отчёт "Daily report" загружен за 87 минут!Среднее время загрузки составило 83 минуты
? Отчёт "Daily report" загружен за 87 минут!Среднее время загрузки составило 83 минуты

Быстро.

Удобно.

Не требует вмешательств и ручных корректировок.

Делаем один раз и наслаждаемся всегда.

Спасибо за просмотр, пишите комментарии, заходите в гости в ТГ канал Детектив данных.

Сподвигла ли вас статья на какие-либо изменения в ваших проектах?

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