«Карманный синоптик за час». Пишем Telegram-бота для мониторинга погоды на Python

0mehhc2hi2cwafe-av2ao4fdfac.png


Хабровчане, всем привет! Меня зовут Максим Плачковский, я автор канала PythonToday. Из этой статьи вы узнаете, как написать своего Telegram-бота для получения данных о погоде в любом городе нашей планеты. Мы детально рассмотрим работу с API, парсинг JSON и напишем бота на асинхронной библиотеке aiogram. А после — загрузим его на виртуальный сервер и запустим. Если интересно, добро пожаловать под кат!

Подготовка API и рабочего окружения


Перед написанием кода нужно получить API-токены для работы с сервисом OpenWeather и Telegram-ботом, а также подготовить рабочее окружение.

Получаем токен OpenWeather


Начнем с самого простого: зарегистрируемся на официальном сайте и в разделе My API keys создадим токен.

mwa4cdt3-_xlukvibst-ytz2ury.png


После того, как вы создали API-ключ, дайте ему немного «отлежаться» — обычно это занимает 10–15 минут. Спустя это время можно общаться с OpenWeather с помощью сгенерированного токена.

Генерируем токен для Telegram-бота


Ключ для Telegram-бота можно получить у @BotFather, введя /newbot  — команду для создания и регистрации нового бота. Во время настройки придумайте боту логин и название — например, Weather Bot. Есть также опциональные настройки: текст приветствия, изображение. Используйте, если хотите получить более уникального бота!

5b8fxqsxxhlx2eknjiwoqynn9ic.png


Импортируем необходимые библиотеки


Для работы нам понадобятся модули requests и aiogram — установим их.

pip install requests aiogram


После импортируем модули и классы в файл нашего пет-проекта.

import os
import datetime
import requests
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor 

bot = Bot(token='your_bot_token')
dp = Dispatcher(bot)


Создание бота


Первое сообщение


Для начала проверим, что aiogram увидел бота и мы можем с ним взаимодействовать. Создадим простую асинхронную функцию start_command для ответа на команду /start . И добавляем метод start_polling для запуска бота.

import os
import datetime
import requests
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor 

bot = Bot(token='your_bot_token')
dp = Dispatcher(bot)

@dp.message.handler(commands=["start"])
async def start_command(message: types.Message):
	await message.reply("Привет! Напиши мне название города и я пришлю сводку погоды")

if __name__ == "__main__":
	# С помощью метода executor.start_polling опрашиваем
    # Dispatcher: ожидаем команду /start
	executor.start_polling(dp)


Запускаем скрипт, заходим в Telegram и пишем команду /start  — все работает: бот возвращает нужное сообщение.

n_odyavf5liscbilm9xu997ujao.png


Обрабатываем входные данные


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

@dp.message_handler()
async def get_weather(message: types.Message):
	pass 


OpenWeather должен принимать название города через API и возвращать данные о погоде. Исходя из документации, кроме названия города запрос должен содержать API-токен.

sbakohb6ve7zao4xtgvtu2uhif4.png


Усовершенствуем запрос: в параметр q будем передавать город, добавим параметр lang=ru, чтобы API работал с кириллицей. А также используем units=metric для установки метрической системы:

http://api.openweathermap.org/data/2.5/weather?q=москва&lang=ru&units=metric&appid=наш_токен


Теперь добавим блок try-except для обработки пользовательских запросов и создадим переменную для записи результатов OpenWeather, которые возвращает сервис в JSON-формате.

@dp.message_handler()
async def get_weather(message: types.Message):
    try:
        response = requests.get(f"http://api.openweathermap.org/data/2.5/weather? q=москва&lang=ru&units=metric&appid=your_token")
        data = response.json()
    except:
        await message.reply("Проверьте название города!")


Отправим запрос через браузер и посмотрим, какая температура, например, в Бангкоке.

1zkx_82wxw0buqhslosijk9s_lk.png


api.openweathermap.org/data/2.5/weather? q=бангкок&lang=ru&units=metric&APPID=your_openweather_token

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

k5h7xuqzyygjvfyumceelhmk65q.png

Парсим JSON


Приступим к парсингу ответа OpenWeather в формате JSON: заберем данные о городе и температуре, влажности, давлении и скорости ветра.

city = data["name"]
cur_temp = data["main"]["temp"]
humidity = data["main"]["humidity"]
pressure = data["main"]["pressure"]
wind = data["wind"]["speed"]


OpenWeather возвращает время рассвета и заката в формате unix timestamp. Извлечем эти данные и преобразуем в секунды.

# получаем время рассвета и преобразуем его в читабельный формат
sunrise_timestamp = datetime.datetime.fromtimestamp(data["sys"]["sunrise"])

# то же самое проделаем со временем заката
sunset_timestamp = datetime.datetime.fromtimestamp(data["sys"]["sunset"])


Зная время рассвета и заката, мы можем вернуть пользователю продолжительность дня. В результате получается следующее:

@dp.message_handler()
async def get_weather(message: types.Message):
    try:
        response = requests.get(f"http://api.openweathermap.org/data/2.5/weather?q=москва&lang=ru&units=metric&appid=your_token")
        data = response.json()
        city = data["name"]
        cur_temp = data["main"]["temp"]
        humidity = data["main"]["humidity"]
        pressure = data["main"]["pressure"]
        wind = data["wind"]["speed"]

        sunrise_timestamp = datetime.datetime.fromtimestamp(data["sys"]["sunrise"])
        sunset_timestamp = datetime.datetime.fromtimestamp(data["sys"]["sunset"])

        # продолжительность дня
        length_of_the_day = datetime.datetime.fromtimestamp(data["sys"]["sunset"]) -       datetime.datetime.fromtimestamp(data["sys"]["sunrise"])

    except:
        await message.reply("Проверьте название города!")


Декорируем сообщения


Разнообразим текст сообщений — будем выводить разные эмодзи в зависимости от погоды.

У словаря, который мы ранее спарсили, есть ключ main  — он хранит описание погоды. На его основе мы можем создать свою коллекцию с эмодзи под разные ситуации.

code_to_smile = {
     "Clear": "Ясно \U00002600",
     "Clouds": "Облачно \U00002601",
     "Rain": "Дождь \U00002614",
     "Drizzle": "Дождь \U00002614",
     "Thunderstorm": "Гроза \U000026A1",
     "Snow": "Снег \U0001F328",
     "Mist": "Туман \U0001F32B"
}


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

# получаем значение погоды
weather_description = data["weather"][0]["main"]

if weather_description in code_to_smile:
    wd = code_to_smile[weather_description]
else:
    # если эмодзи для погоды нет, выводим другое сообщение
    wd = "Посмотри в окно, я не понимаю, что там за погода..."


Возможно, эти тексты тоже вас заинтересуют:

→ Как подключить платежную систему с Payments к Telegram
→ Как общаться с ChatGPT с помощью голосовых сообщений в Telegram
→ Простая процедурная генерация мира, или Шумы Перлина на Python


Возвращаем данные пользователю


Данные собрали — время отдать их пользователю. Сформируем строки с датой и временем, погодой, влажностью и другими данными. Главное — учесть формат данных. Например, OpenWeather возвращает значение давления в гектопаскалях. Так, если вы хотите отдавать пользователю данные в миллиметрах ртутного столба, значение нужно разделить на 1.33 и округлить в большую сторону.

await message.reply(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"\n
     f"Погода в городе: {city}\nТемпература: {cur_weather}°C {wd}\n"
     f"Влажность: {humidity}%\nДавление: {math.ceil(pressure/1.333)} мм.рт.ст\nВетер: {wind} м/с \n"
     f"Восход солнца: {sunrise_timestamp}\nЗакат солнца: {sunset_timestamp}\nПродолжительность дня: {length_of_the_day}\n"
     f"Хорошего дня!"
)


Супер — бот работает и возвращает данные в удобочитаемом формате!

bzsiqx4-obkxrswy9sdauofc_xa.png


Деплой бота


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

Бота лучше перенести в облако. Поскольку затраты процессора на работу с простым Open Weather API минимальны, будет достаточно виртуального сервера с 1 vCPU и 1 ГБ оперативной памяти. С учетом выделенного IP-адреса такая конфигурация выйдет примерно в 30 ₽/день.

Для начала зарегистрируемся в панели управления и создадим новый сервер в разделе Облачная платформа. Затем настроим его.

r-j31wdvj8exoryil8w4fvjotaq.png


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

xh0c4sikstsj2wb5fwq_yenuip8.png


Последним шагом нужно создать .service-файл, который будет автоматически поднимать бота в случае перезагрузки сервера.

b0zwxoor4qxfmhzne5xulvqcfmc.png


xluah-w8wqjqyekdz1dg3pede_s.png


Запускаем командой: systemctl enable tg_bot.service

Проверяем статус: systemctl status tg_bot.service

И перезапускаем .service-файл: systemctl restart tg_bot.service

ye62yx1r8gov8yrxqw-4tp05w7y.png


Все готово: бот стабильно работает на сервере и автоматически поднимается в случае перезагрузки.

us5wsgmkni1c3au7wk09oeynes0.png


Как мы видим, в разработке подобных Telegram-ботов нет ничего сложного. Также они не так затратны, как может показаться: для хостинга проекта не нужно платить полную стоимость сервера за месяц — в облаке оплачиваются только потребленные ресурсы по модели pay-as-you-go. Предлагайте идеи для новых ботов в комментариях!

© Habrahabr.ru