
Всем привет! Зовут меня Виталий, автор тг канала Детектив данных про мой вкат в аналитику данных.
Сегодня я хочу поделиться с вами одной полезной функцией, которую когда-то придумал для себя и теперь активно использую в своей работе. Это такая мини-надстройка, которая помогает сделать регулярные отчёты немного приятнее и функциональнее, хоть это и не обязательное улучшение.
С помощью 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, покопаемся в мелочах и отправим себе удобную картинку с анализом нашей загрузки. Жду ваших эмоций к посту, комментариев - и до скорой встречи в новых статьях!
Ну а теперь самое интересное - отрисуем график загрузки

Давайте договоримся о переменных. Почти все они уже были определены в прошлый раз, и сейчас мы их просто обозначим для независимости первой части стати от второй статей друг от друга.
наименование отчёта - это нужно для фильтрации в дальнейшем
токен ТГ бота
наш чат айди с ботом
и время загрузки отчета
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()
Результат еще раз:

Быстро.
Удобно.
Не требует вмешательств и ручных корректировок.
Делаем один раз и наслаждаемся всегда.
Спасибо за просмотр, пишите комментарии, заходите в гости в ТГ канал Детектив данных.
Сподвигла ли вас статья на какие-либо изменения в ваших проектах?