Запросы к внешним API (aiohttp)

Учим «Цыплёнка-помощника» выходить в большой интернет: спрашивать у чужих сервисов погоду, курс валют или случайный мем — и приносить ответ тебе в чат.
API (Application Programming Interface) — это «окошко выдачи» чужой программы: ты присылаешь туда запрос по строгим правилам, а в ответ получаешь готовые данные, обычно в виде JSON.

Хук: бот, который знает то, чего не знаешь ты

Представь: завтра ты собираешься гулять с друзьями, но за окно лень выглядывать. Ты пишешь «Цыплёнку-помощнику»: «погода в Казани», а он через секунду отвечает: «Сейчас +4°, облачно, бери куртку». Откуда бот это узнал? Он же не метеоролог и не сидит у окна с термометром.

Секрет в том, что где-то в интернете есть сервис, который и правда следит за погодой по всему миру. У него есть API — специальное «окошко», куда можно постучаться и спросить: «а что сейчас в Казани?». Наш бот стучится туда, получает ответ и пересказывает его тебе. То же самое умеют делать боты, которые показывают курс доллара, присылают случайный мем, проверяют расписание автобуса или достают факт дня из «Википедии».

До сих пор наш Цыплёнок жил в своём маленьком мирке: он умел отвечать только тем, что мы сами зашили в код (вспомни урок Хэндлеры сообщений — там бот реагировал на готовые фразы). Сегодня мы откроем ему дверь в большой интернет. К концу урока твой бот сможет сходить за свежими данными на чужой сервер, разобрать ответ и красиво показать его в чате. И главное — не упасть, если интернет вдруг отвалится.

Что такое API и запрос к нему

Давай разберёмся со страшным словом API на понятном примере. Представь столовую с окошком выдачи. Ты не лезешь на кухню сам — ты подходишь к окошку и говоришь по правилам: «один борщ, пожалуйста». Повар на кухне готовит и выдаёт тебе тарелку. Ты не знаешь, как устроена их плита и где они берут свёклу, — тебе и не надо. Главное — знать правила заказа: к какому окошку подойти и что сказать.

API — это и есть такое окошко выдачи у чужой программы. Сервис погоды, ВКонтакте, банк, «Википедия» — у каждого есть своё окошко с правилами: по какому адресу обращаться и что прислать в запросе. А запрос (по-английски request) — это твой «заказ» в это окошко.

Зачем это вообще придумали? Подумай: команда метеорологов годами собирала данные со спутников и тысяч датчиков. Команда «Википедии» написала миллионы статей. Ты не сможешь повторить эту работу в своём боте — да и не надо. API позволяет тебе взять готовый результат чужого труда и использовать его у себя. Это как пользоваться розеткой: ты не строишь электростанцию, ты просто втыкаешь вилку и получаешь ток. Почти у каждого крупного сервиса в интернете есть API — именно поэтому в Telegram столько ботов, которые умеют переводить тексты, показывать котировки акций, искать картинки и присылать гороскоп. Все они под капотом стучатся в чужие окошки.

Важно понять: твой бот в этой истории — не повар, а посетитель. Он сам подходит к чужому окошку и делает заказ. А вот когда кто-то пишет твоему боту в Telegram — уже твой бот становится поваром у своего окошка. Так что один и тот же бот одновременно и обслуживает пользователей, и сам бегает к другим сервисам за данными. Сегодня нас интересует именно вторая роль.

Адрес, на который стучимся

У каждого окошка есть адрес — обычная ссылка, как у сайта. Например, тестовый сервис погоды может принимать запросы по адресу https://api.open-meteo.com/v1/forecast?latitude=55.8&longitude=49.1&current=temperature_2m. Всё, что после знака ? — это уточнения к заказу: «широта такая-то, долгота такая-то, верни текущую температуру». Это похоже на то, как ты в окошко говоришь не просто «борщ», а «борщ без сметаны и побольше».

Что приходит в ответ: JSON

Сервис почти всегда отвечает в формате JSON. Не пугайся — это просто текст, который выглядит как знакомый тебе словарь Python с фигурными скобками. Вот как может выглядеть ответ сервиса погоды:

{
  "latitude": 55.8,
  "longitude": 49.1,
  "current": {
    "temperature_2m": 4.2,
    "time": "2026-06-19T12:00"
  }
}

Видишь? Это почти словарь: ключи в кавычках, значения после двоеточия, всё во вложенных скобках. Наша задача — достать из этого ответа нужное число (например, temperature_2m) и показать его пользователю.

Делаем запрос: библиотека aiohttp

Возможно, ты слышал про библиотеку requests — ей часто учат делать запросы в обычном Python. Но у нас асинхронный бот на aiogram, и тут requests не подходит: она «блокирующая». Это как официант, который, приняв твой заказ, встаёт столбом у кухни и ждёт борщ, не обслуживая остальные столики. Пока он ждёт — весь зал простаивает. Если наш бот так зависнет на запросе к погоде, он перестанет отвечать всем остальным пользователям.

Поэтому мы берём aiohttp — асинхронную библиотеку для запросов. Её официант, отправив заказ на кухню, не торчит у окошка, а бежит обслуживать другие столики, а когда борщ готов — возвращается за ним. Так бот успевает отвечать многим людям одновременно. Установить её можно командой pip install aiohttp.

Может возникнуть вопрос: «а зачем вообще эта асинхронность, у меня же бот для друзей, пять человек максимум?» Резонно — но привычку лучше выработать сразу. Представь бота в чате игрового клана на полсотни человек: после рейда все разом жмут /loot, и каждый запрос лезет в сеть за данными. С блокирующей библиотекой бот обслуживал бы их по очереди, и последний в очереди ждал бы ответа минуту. С aiohttp все запросы идут «внахлёст», и каждый получает ответ почти мгновенно. Асинхронность — это не усложнение ради усложнения, а способ не заставлять людей ждать.

Сниппет: как мы достанем число из словаря

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

answer = {
    "current": {
        "temperature_2m": 4.2,
        "time": "2026-06-19T12:00"
    }
}

temp = answer["current"]["temperature_2m"]
print(f"Сейчас {temp}°")

Вывод:

Сейчас 4.2°

Вот и весь секрет: ответ сервиса — это вложенный словарь, и мы идём по ключам, как по этажам: сначала current, потом внутри него temperature_2m. Когда ты это понял, остаётся научиться получать такой словарь из интернета.

Разбор на примерах

Пример 1. Первый асинхронный GET-запрос

Запрос «дай мне данные» называется GET-запросом (по-английски — «получить»). Добавим в наш bot.py команду /weather, которая сходит за погодой. Разберём по строчкам:

import aiohttp
from aiogram.filters import Command
from aiogram.types import Message

URL = "https://api.open-meteo.com/v1/forecast?latitude=55.8&longitude=49.1&current=temperature_2m"

@dp.message(Command("weather"))
async def cmd_weather(message: Message):
    async with aiohttp.ClientSession() as session:
        async with session.get(URL) as response:
            data = await response.json()
    temp = data["current"]["temperature_2m"]
    await message.answer(f"Сейчас в Казани {temp}°")

Результат: в чате бот ответит «Сейчас в Казани 4.2°» (число будет настоящим, актуальным на момент запроса). Разберём незнакомые места:

  • aiohttp.ClientSession() — это сессия, наш «официант». Через неё проходят все запросы. Мы открываем её через async with, чтобы Python сам аккуратно закрыл её, когда мы закончим.
  • session.get(URL) — отправляем GET-запрос на наш адрес. Тоже в async with — это окошко, которое надо закрыть.
  • await response.json() — берём ответ и сразу превращаем его из текста-JSON в словарь Python. Слово await здесь значит «дождись, пока данные приедут, но не блокируй остальных».
  • Дальше — уже знакомый разбор словаря по ключам.

Пример 2. Город по выбору пользователя

Хардкодить Казань скучно — пусть пользователь сам пишет город. Многие API принимают название города прямо в адресе. Сделаем так, чтобы координаты или название подставлялись в URL. Для наглядности возьмём сервис, который ищет город по имени:

@dp.message(Command("weather"))
async def cmd_weather(message: Message):
    # всё, что после /weather, считаем названием города
    city = message.text.replace("/weather", "").strip()
    if not city:
        await message.answer("Напиши так: /weather Казань")
        return

    geo_url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1"
    async with aiohttp.ClientSession() as session:
        async with session.get(geo_url) as response:
            geo = await response.json()

    results = geo.get("results")
    if not results:
        await message.answer(f"Не нашёл город «{city}». Проверь название.")
        return

    place = results[0]
    await message.answer(f"Нашёл: {place['name']}, координаты {place['latitude']}, {place['longitude']}")

Результат: на /weather Казань бот ответит «Нашёл: Kazan, координаты 55.79, 49.12». А на /weather абвгд — «Не нашёл город «абвгд». Проверь название.» Обрати внимание на geo.get("results"): мы используем .get(), а не квадратные скобки, потому что ключа results в ответе может и не быть — и тогда вместо падения мы получим None и вежливо предупредим пользователя.

Пример 3. Защищаемся от падений

Интернет — штука ненадёжная. Сервер может не ответить, пропасть связь, прийти мусор вместо JSON. Если не подстелить соломку, бот упадёт с ошибкой прямо посреди разговора. Завернём запрос в try / except:

import asyncio

@dp.message(Command("weather"))
async def cmd_weather(message: Message):
    try:
        timeout = aiohttp.ClientTimeout(total=5)
        async with aiohttp.ClientSession(timeout=timeout) as session:
            async with session.get(URL) as response:
                if response.status != 200:
                    await message.answer("Сервис погоды занят, попробуй позже.")
                    return
                data = await response.json()
        temp = data["current"]["temperature_2m"]
        await message.answer(f"Сейчас в Казани {temp}°")
    except asyncio.TimeoutError:
        await message.answer("Погода долго не отвечает. Попробуй ещё раз.")
    except aiohttp.ClientError:
        await message.answer("Не получилось связаться с сервисом погоды :(")

Результат: в обычной ситуации бот по-прежнему отвечает «Сейчас в Казани 4.2°». Но если интернет пропал — он напишет «Не получилось связаться с сервисом погоды :(», если сервис думает дольше 5 секунд — «Погода долго не отвечает», а если сервер вернул ошибку — «Сервис погоды занят, попробуй позже». Бот остаётся жив и продолжает обслуживать остальных. Разберём защиту:

  • ClientTimeout(total=5) — ставим таймаут: «жди ответа не дольше 5 секунд». Без него бот мог бы зависнуть навечно, если сервис молчит.
  • response.status — это код ответа. 200 значит «всё хорошо». Если пришло что-то другое (например, 404 или 500) — данных нет, и лезть в словарь не стоит.
  • except asyncio.TimeoutError и except aiohttp.ClientError — ловим типичные сетевые беды. ClientError — это общий «родитель» сетевых ошибок aiohttp, он накроет большинство проблем со связью.

Частые ошибки и подводные камни

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

  • Забыли await у response.json(). Без await ты получишь не словарь, а «корутину» — странный объект, из которого ничего не достать. Правило: всё, что лезет в сеть (session.get, response.json), нужно «ждать» через await.
  • Открывают новую ClientSession на каждый запрос внутри цикла. Создавать сессию дорого. Для одного-двух запросов в хэндлере это нормально, но если делаешь десятки запросов подряд — открой одну сессию и переиспользуй её, иначе бот будет тормозить.
  • Лезут в словарь по ключу, которого нет. Запись data["current"]["temperature_2m"] упадёт с KeyError, если сервис вернул ошибку вместо погоды. Сначала проверь response.status и наличие нужных ключей (через .get()), а уже потом доставай данные.
  • Никак не обрабатывают обрыв связи. На твоём компьютере с хорошим интернетом всё работает. Но на сервере связь иногда пропадает, и запрос без try / except уронит хэндлер. Всегда оборачивай сетевые запросы в обработку ошибок.
  • Путают, кому нужен await, а кому нет. Разбор уже полученного словаря (data["current"]) — обычный код, ему await не нужен. await ставится только перед сетевыми операциями, которые «ходят наружу» и могут подождать.
  • Прячут ключ-доступа прямо в коде. Многие API требуют свой ключ-доступа (API-key). Это секрет, как и токен бота, — его место в переменных окружения, а не в строке URL, которую ты потом случайно выложишь на GitHub.
  • Шлют запросы слишком часто. У бесплатных API почти всегда есть лимит: например, не больше 60 запросов в минуту. Если на каждое сообщение в чате дёргать сервис, лимит быстро кончится, и тебя на время заблокируют. Полезно кешировать ответ: узнал погоду один раз — подержи её несколько минут, а не спрашивай заново на каждый /weather.

Мини-практика: бот «факт дня»

Закрепим всё на маленьком проекте. Сделай для Цыплёнка команду /fact, которая приносит случайный факт. Есть бесплатный сервис, который отдаёт случайный факт по адресу https://uselessfacts.jsph.pl/api/v2/facts/random?language=en, а ответ выглядит так:

{
  "id": "abc123",
  "text": "Octopuses have three hearts.",
  "source": "..."
}

Твоя задача:

  1. Добавь хэндлер на команду /fact (по образцу cmd_weather).
  2. Сделай GET-запрос через aiohttp.ClientSession и разбери ответ через await response.json().
  3. Достань из словаря значение по ключу "text" и отправь его пользователю.
  4. Заверни всё в try / except, чтобы бот не упал, если сервис недоступен — пусть в этом случае ответит «Факты закончились, зайди позже :)».
  5. Со звёздочкой: поставь таймаут в 5 секунд и проверь response.status == 200 перед тем, как читать JSON.

Подсказка: структура хэндлера почти точь-в-точь как в Примере 3, меняются только URL и ключ, который ты достаёшь из словаря ("text" вместо вложенной температуры). Если справишься — у тебя в руках универсальный шаблон для общения с любым API.

Итоги и что дальше

Соберём главное:

  • API — это «окошко выдачи» чужого сервиса: присылаешь запрос по правилам, получаешь данные.
  • Сервисы отвечают в формате JSON — это текст, который в Python становится обычным вложенным словарём.
  • Запросы из бота делаем через aiohttp, а не requests: он асинхронный и не блокирует бота, пока ждёт ответа.
  • Базовый шаблон: открыть ClientSession → сделать session.get(url) → взять await response.json() → достать значение по ключам.
  • await ставим только перед сетевыми операциями; разбор готового словаря в нём не нуждается.
  • Сеть ненадёжна, поэтому обязательны таймаут, проверка response.status и try / except — без них бот упадёт при первом же обрыве связи.

Теперь Цыплёнок умеет выходить в большой интернет и приносить оттуда свежие данные — это огромный шаг от «бота, который знает только зашитые фразы» к «боту, который знает мир». В следующем уроке мы пойдём дальше: научимся аккуратно собирать из полученных данных красивые сообщения и разберём ещё несколько полезных публичных API, на которых можно построить классные фишки. Бери куртку по совету бота — и увидимся в следующем уроке!

Проверьте себя
1. Что такое API простыми словами?
AЭто формат, в котором Telegram хранит сообщения на своих серверах
BЭто «окошко выдачи» чужой программы: присылаешь запрос по правилам — получаешь данные
CЭто специальный язык программирования для ботов
DЭто папка с файлами на твоём компьютере
2. Почему в асинхронном боте на aiogram для запросов берут aiohttp, а не requests?
AПотому что requests не умеет работать с JSON
BПотому что requests платная, а aiohttp бесплатная
CПотому что requests блокирующая и заставит бота зависнуть, пока он ждёт ответ, не обслуживая других
DПотому что aiohttp быстрее скачивает большие файлы
3. Что делает строка data = await response.json()?
AОтправляет на сервер новый запрос
BПревращает текст-JSON из ответа в обычный словарь Python, дождавшись данных
CСохраняет ответ в файл на диске
DЗакрывает соединение с сервером
4. Зачем оборачивать сетевой запрос в try / except и ставить таймаут?
AЧтобы запрос выполнялся быстрее
BЭто требование Telegram, иначе бота заблокируют
CЧтобы бот не упал и не завис, если сервис недоступен или долго молчит
DЧтобы скрыть токен бота от посторонних
5. Перед какими операциями в коде запроса нужно ставить await?
AПеред любой строкой кода без исключений
BТолько перед сетевыми операциями вроде session.get() и response.json()
CТолько перед обращением к ключам словаря
Dawait вообще не нужен в aiohttp
6. Почему для чтения ключа из ответа сервиса безопаснее использовать .get("results") вместо квадратных скобок?
AПотому что .get() работает быстрее квадратных скобок
BПотому что квадратные скобки запрещены в асинхронном коде
CПотому что если ключа в ответе нет, .get() вернёт None, а скобки уронят бота с KeyError
DПотому что .get() автоматически делает новый запрос к серверу