Бот-погода
Учим Цыплёнка спрашивать город и приносить из интернета настоящую погоду — чтобы перед прогулкой не гадать, брать ли куртку.
Бот-погода — это бот, который принимает от тебя название города, через
aiohttpходит к внешнему погодному API, достаёт из ответа температуру и описание неба и присылает их человеческим языком.
Зачем это нужно
Представь утро перед школой. Ты ещё в кровати, телефон под подушкой, а на улице непонятно что: то ли солнце, то ли дождь стеной. Открывать приложение погоды лень — там реклама, виджеты, прогноз на десять дней, а тебе нужно одно: брать куртку или нет. Вот бы написать в чат одно слово — Казань — и сразу получить ответ: «+7, облачно, накинь толстовку».
Именно это мы сегодня и сделаем. Наш Цыплёнок-помощник уже умеет здороваться, показывать кнопки, вести диалог через FSM и даже запоминать тебя в базе. В прошлом уроке про запросы к API через aiohttp ты научил его стучаться в чужие серверы и приносить оттуда данные. Сегодня мы используем этот навык по-настоящему: научим Цыплёнка ходить за погодой и отвечать так, будто он сам выглянул в окно.
Вот к чему мы придём. Ты пишешь боту название города, он думает секунду и отвечает:
Ты: Казань
Цыплёнок: 🐤 В городе Казань сейчас +7°C, облачно с прояснениями.
Ощущается как +4°C. Бери толстовку!Никакой магии — обычный HTTP-запрос плюс аккуратный разбор ответа. Поехали.
Откуда бот берёт погоду
Сам по себе бот про погоду ничего не знает. У него нет градусника за окном и нет спутника на орбите. Зато в интернете есть сервисы, которые всё это держат у себя и готовы поделиться данными — нужно только вежливо попросить. Такой сервис называется API: набор HTTP-запросов, через которые твоя программа спрашивает у чужого сервера и получает ответ.
Представь библиотекаря за стойкой. Ты не лезешь сам на склад книг — ты подходишь и говоришь: «Дайте, пожалуйста, погоду в Казани». Библиотекарь уходит, возвращается с карточкой и читает: «+7, облачно». Погодный API — это и есть такой библиотекарь. Ты шлёшь ему запрос с названием города, он отдаёт тебе аккуратную карточку с данными.
Карточку он отдаёт не словами, а в формате JSON — это текст, который выглядит как вложенные словари и списки Python. Внутри лежат температура, описание неба, влажность и куча всего ещё. Наша задача — достать оттуда только нужное.
Мы возьмём бесплатный сервис wttr.in: у него есть режим, который отдаёт погоду в JSON, и ему не нужен отдельный ключ — удобно для учёбы. В реальном проекте чаще берут OpenWeatherMap с токеном, но принцип ровно тот же: шлём запрос, читаем JSON, достаём поля.
Запомни картинку: бот — официант. Ты сказал «погоду в Казани», официант сбегал на кухню Telegram и дальше к погодному сервису, принёс блюдо и поставил перед тобой. Async тут именно для того, чтобы официант, пока бегает за твоим заказом, успевал обслужить и других гостей чата.
Разбираем по шагам
Шаг 1. Тренируемся разбирать JSON без интернета
Прежде чем лезть в сеть, давай поймём, как вообще достать данные из ответа. Сервер пришлёт нам словарь, а в Python разбор словаря — простейшая операция. Этот сниппет можно запустить прямо здесь: он не ходит в интернет, а работает с заранее заготовленным «ответом сервера».
fake_answer = {
"current_condition": [
{
"temp_C": "7",
"FeelsLikeC": "4",
"weatherDesc": [{"value": "Partly cloudy"}],
}
]
}
current = fake_answer["current_condition"][0]
temp = current["temp_C"]
feels = current["FeelsLikeC"]
desc = current["weatherDesc"][0]["value"]
print(f"Температура: {temp}°C")
print(f"Ощущается как: {feels}°C")
print(f"Небо: {desc}")Вывод:
Температура: 7°C Ощущается как: 4°C Небо: Partly cloudy
Смотри внимательно на путь к данным. current_condition — это список, поэтому сразу после него идёт [0]: берём первый (и единственный) элемент. Внутри — словарь, из него по ключу достаём temp_C. А weatherDesc снова список, поэтому опять [0], и уже внутри ключ value. Эта «лесенка» из квадратных скобок и есть главный навык работы с API: ты идёшь по структуре ответа сверху вниз, как по этажам.
Шаг 2. Делаем настоящий запрос через aiohttp
Теперь подключаем интернет. Запрос к серверу — операция небыстрая, поэтому в асинхронном боте мы используем aiohttp и ждём ответ через await, не блокируя остальных. Этот код уже часть бота, поэтому он помечен как обычный текст — в браузере его не запустить, ему нужен живой Telegram и сеть.
import aiohttp
async def get_weather(city: str) -> dict | None:
url = f"https://wttr.in/{city}"
params = {"format": "j1"}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response:
if response.status != 200:
return None
return await response.json()Результат: функция сходит на wttr.in, попросит погоду в формате j1 (это JSON) и вернёт большой словарь с данными. Если сервер ответил не «200 ОК» (например, города нет или сервис лёг), вернётся None — это наш сигнал «не получилось».
Разберём ключевые места. async with aiohttp.ClientSession() открывает сессию — это как телефонная линия до сервера; async with гарантирует, что линию аккуратно положат, даже если случится ошибка. session.get(...) отправляет запрос и через await ждёт ответ. response.status — код ответа: 200 значит «всё хорошо». А await response.json() превращает присланный текст в обычный словарь Python — тот самый, который мы тренировались разбирать в шаге 1.
Шаг 3. Собираем красивое сообщение
Сырой словарь человеку показывать нельзя — он испугается. Сделаем отдельную функцию, которая достаёт нужные поля и лепит из них тёплый текст. Заметь: описание неба у wttr.in приходит по-английски, поэтому простой словарь-переводчик превратит хотя бы частые состояния в русские слова.
def format_weather(city: str, data: dict) -> str:
current = data["current_condition"][0]
temp = current["temp_C"]
feels = current["FeelsLikeC"]
desc_en = current["weatherDesc"][0]["value"]
translate = {
"Sunny": "солнечно",
"Clear": "ясно",
"Partly cloudy": "облачно с прояснениями",
"Cloudy": "облачно",
"Overcast": "пасмурно",
"Light rain": "небольшой дождь",
}
desc = translate.get(desc_en, desc_en)
advice = "Бери толстовку!" if int(temp) < 12 else "Можно идти налегке."
return (
f"🐤 В городе {city} сейчас {temp}°C, {desc}.\n"
f"Ощущается как {feels}°C. {advice}"
)Результат: функция вернёт готовую строку вроде «🐤 В городе Казань сейчас +7°C, облачно с прояснениями. Ощущается как +4°C. Бери толстовку!». Метод translate.get(desc_en, desc_en) хитрый: если перевод для состояния нашёлся — берём его, если нет — оставляем как было, чтобы бот не молчал на редкой погоде.
Шаг 4. Связываем всё в хэндлере
Осталось соединить части в хэндлере — функции, которую aiogram вызовет, когда пользователь пришлёт боту название города. Объекты bot и dp у нас уже созданы в bot.py из прошлых уроков, токен мы по-прежнему держим в переменной окружения. Добавляем новый кусок к тому же боту.
from aiogram import F
from aiogram.types import Message
@dp.message(F.text)
async def weather_handler(message: Message):
city = message.text.strip()
await message.answer(f"Секунду, узнаю погоду в городе {city}...")
data = await get_weather(city)
if data is None:
await message.answer("Не нашёл такой город 😢 Проверь название и попробуй ещё раз.")
return
text = format_weather(city, data)
await message.answer(text)Результат: когда ты пишешь боту «Казань», он сперва отвечает «Секунду, узнаю погоду в городе Казань...», затем сходит за данными и присылает готовый прогноз. Если города нет — мягко попросит проверить название.
Обрати внимание на F.text: это фильтр aiogram, который означает «срабатывай на любое текстовое сообщение». А message.text.strip() убирает случайные пробелы по краям — пользователи вечно ставят лишний пробел, и без strip() сервер мог бы не найти город.
Частые ошибки и подводные камни
- Забыть, что
current_condition— список. Самая популярная ошибка новичков: написатьdata["current_condition"]["temp_C"]и получитьTypeError. Между ключом и температурой обязательно стоит[0], потому что сервер отдаёт список из одного элемента. Всегда смотри на структуру ответа, прежде чем лезть в неё. - Не проверять
statusи не ловитьNone. Если сервер прилёг или города нет, а ты сразу зовёшьformat_weatherна пустых данных — бот упадёт с ошибкой, а пользователь увидит молчание. Проверкаif data is Noneв хэндлере спасает от этого: лучше вежливое «не нашёл город», чем зависший бот. - Использовать
requestsвместоaiohttp. Старый знакомыйrequestsработает синхронно: пока он ждёт ответ от сервера, весь бот замирает и не отвечает другим. В асинхронном боте это всё равно что официант, который замер посреди зала и ждёт. Внутриasync-функции ходи в сеть только черезaiohttpиawait. - Сравнивать температуру как строку. Сервер присылает температуру строкой:
"7", а не числом7. Если написатьtemp < 12, Python ругнётся, что нельзя сравнивать строку с числом. Поэтому в коде стоитint(temp)— сначала превращаем в число, потом сравниваем. - Открывать новую сессию на каждый чих и не закрывать её. Если создавать
ClientSession()безasync withи не закрывать, со временем накопятся «висящие» соединения и бот начнёт сыпать предупреждениями. Конструкцияasync withзакрывает линию автоматически — пользуйся ей.
Мини-практика
Базовый Цыплёнок-метеоролог готов. Теперь доведи его до ума сам — выбери задачу по силам:
- Эмодзи под погоду. Добавь в
format_weatherсловарь, который к описанию подбирает эмодзи: солнечно → ☀️, облачно → ☁️, дождь → 🌧️. Подставь его в начало сообщения. - Совет по одежде поточнее. Сейчас совет грубый: одна граница на 12 градусов. Сделай несколько уровней: ниже 0 — «надень шапку», 0–12 — «толстовка», выше 20 — «футболка и вода с собой».
- Память города (со звёздочкой). Вспомни прошлые уроки про SQLite: сохраняй последний запрошенный город пользователя в базу, а по команде
/lastпоказывай погоду в нём без повторного ввода. Так бот станет по-настоящему личным.
Не гонись сразу за всеми тремя — сделай первую, проверь в чате, что бот отвечает с эмодзи, и только потом берись за следующую. Маленькими шагами надёжнее.
Итоги
Сегодня Цыплёнок-помощник получил мощную способность: он перестал быть закрытым в себе и научился приносить данные из большого интернета. Ты прошёл весь путь запроса — от голого JSON, который разобрал руками, до живого aiohttp-запроса, аккуратного разбора ответа и тёплого сообщения с советом про куртку. Заодно ты увидел, почему в асинхронном боте важно ходить в сеть через aiohttp, а не requests, и как защититься от «нет такого города».
Главное, что ты унёс: любой внешний сервис — это библиотекарь за стойкой. Ты шлёшь вежливый запрос, получаешь карточку-JSON и достаёшь из неё нужное по «лесенке» из ключей и индексов. С этим навыком тебе открыты тысячи API.
В следующем уроке мы научим Цыплёнка ходить уже за новостями: принцип тот же — запрос, JSON, разбор, — но данных будет больше, и ты увидишь, как красиво показывать список из нескольких элементов, а не одно значение. До встречи!