Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 13 из 20 Средний 30 мин 130 XP

Что отдаёт API: ответ как текст

API отвечает текстом со статусом и заголовками: берём заранее сохранённый настоящий ответ — и учимся мерить его длину, искать подстроки и превращать байты в строку.

Редакция Питоники

Восемь уроков мы работали с JSON, который кто-то любезно положил нам в переменную. Пора посмотреть, откуда он берётся. Запрос к API по URL из прошлого урока возвращает HTTP-ответ — а это не словарь и не «данные», это конверт: строка статуса, блок заголовков и тело, в котором JSON приходит обычным текстом. Сегодня разбираем конверт. Ответ API — это сначала текст со статусом, и только после json.loads — данные, с которыми можно работать.

Одно честное уточнение сразу. Браузерная песочница, в которой исполняется код на странице, не имеет доступа к сети — настоящий запрос из неё не отправить. Поэтому работать будем с заранее сохранённым настоящим ответом: текст, который сервер отдал на реальный запрос, записан в константу. Каждая буква в нём настоящая — просто конверт доставлен заранее.

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

Сохранённый ответ: это строка

Вот ответ сервиса погоды, сохранённый в константу так же, как его сохраняет в .text библиотека requests. Проверим тип и померим длину — как для любой строки:

сохранённый ответ API
API_RESPONSE = (
    '{"city": "Москва", "temp": 21, "unit": "C", '
    '"sky": "облачно", "wind": 3}'
)

print(type(API_RESPONSE))
print(len(API_RESPONSE))
print(API_RESPONSE[:30])
Вывод
<class 'str'>
72
{"city": "Москва", "temp": 21,

<class 'str'> — запомни эту строку: до разбора ответ — просто текст. Семьдесят два символа — и срез [:30] показывает первые из них: знакомый JSON, но пока без единой сверхспособности. Со строкой можно срезать, искать, мерить длину — и нельзя доставать поля: ключей у текста нет.

В сыром виде ответ начинается строкой статуса: HTTP/1.1 200 OK — версия протокола, код и человеческое пояснение. Дальше идут заголовки, пустая строка-разделитель и тело. Библиотеки режут этот конверт на части заранее: статус прячут в .status_code, заголовки — в .headers, тело — в .text. Но видеть исходную форму полезно: становится ясно, откуда взялись все эти атрибуты.

Статус: сначала код, потом тело

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

СтатусЗначитЧто делать
200успех, тело с даннымиразбирать
301 / 302адрес переехалбиблиотека следует сама
404такого адреса нетпроверять URL из урока 12
500сервер ошибсяповторить позже, тело может быть текстом ошибки
проверка статуса перед разбором
API_STATUS = 200
API_RESPONSE = '{"city": "Москва", "temp": 21}'

if API_STATUS == 200:
    print("Ответ получен, можно разбирать")
    print("Тело:", API_RESPONSE)
else:
    print("Ошибка запроса, статус", API_STATUS)
Вывод
Ответ получен, можно разбирать
Тело: {"city": "Москва", "temp": 21}

Но статус не всегда 200 — и это не повод падать: сервер на 404 тоже отвечает телом, просто внутри не данные, а описание ошибки. Его тоже можно разобрать и понять, что случилось:

Тройки (301, 302) — «адрес переехал»: библиотеки обычно следуют перенаправлению сами, и ты даже не замечаешь лишнего хода. Вручную этот класс статусов обрабатывают редко, но знать его стоит: если в логах мелькают 301, значит URL где-то устарел, и правильнее починить адрес по уроку 12, чем возить каждый запрос лишним кругом.

сервер ответил ошибкой
SAVED_STATUS = 404
SAVED_BODY = '{"error": "page not found"}'

if SAVED_STATUS == 200:
    print("Успех:", SAVED_BODY)
else:
    print("Сервер ответил ошибкой", SAVED_STATUS, "- читаем текст ошибки")
    print(SAVED_BODY)
Вывод
Сервер ответил ошибкой 404 - читаем текст ошибки
{"error": "page not found"}

Две тонкости про имена заголовков. Во-первых, HTTP делает регистр неразличимым: Content-Type и content-type — один заголовок, и библиотеки сами приводят ключи к нижнему регистру. Во-вторых, обычный словарь Python так не умеет: там это два разных ключа, поэтому в своих константах держи имена в одном стиле, как в примере выше.

Отдельный статус 204 — «успех без содержания»: сервер отвечает без тела вообще, и len тела честно даст ноль. Не всякий успех обязывает что-то разбирать — иногда весь ответ умещается в код. Перед loads полезная привычка: если тела нет, разбирать нечего — эта проверка из следующего урока станет первой строкой надёжного разбора.

Тело как текст: поиск и длина

Раз тело — строка, с ним работают строковые инструменты: in проверяет подстроку, find находит позицию, count считает вхождения. Это удобно для быстрых проверок ещё до разбора: пришло ли то, чего ждали.

строковые проверки тела
API_RESPONSE = '{"city": "Москва", "temp": 21}'

print("temp" in API_RESPONSE)
print(API_RESPONSE.find('"temp"'))
print(API_RESPONSE.count(":"))
Вывод
True
19
2

find вернул 19 — позицию, с которой начинается фрагмент с ключом temp внутри текста. Полезно на практике для логов: попало ли в тело слово error, пришёл ли ожидаемый ключ. Но данные так не достают — поиск по строке ничего не знает о структуре JSON; за структурой нужен разбор из следующего урока.

Байты и кодировка

Тонкость, о которой библиотеки молчат: по сети едут байты, а не буквы. Текст из байтов собирается методом decode с именем кодировки — сервер честно сообщает её в заголовке Content-Type. Проведём путь байтов своими руками:

байты превращаются в строку
RAW = b'{"city": "Kazan", "temp": 18}'

print(type(RAW))
text = RAW.decode("utf-8")
print(type(text))
print(text)
Вывод
<class 'bytes'>
<class 'str'>
{"city": "Kazan", "temp": 18}

Буква b перед строкой — запись байтов. У байтов нет кодировки: это просто числа; кодировка появляется в момент decode. Если декодировать не utf-8, а чем-то другим, кириллица превратится в кракозябры — самая знаменитая ошибка работы с чужими API.

Из пары «строка и байты» следует практическое следствие: длина у них разная. Кириллическая буква в utf-8 весит два байта, поэтому байтовое тело всегда длиннее своего текста — и это не баг, а честный размер того, что поехало по сети:

символов и байтов
body = '{"city": "Москва"}'

print(len(body))
print(len(body.encode("utf-8")))
Вывод
18
24

Восемнадцать символов превратились в двадцать четыре байта: каждая кириллическая буква добавила по лишнему байту. Если размер тела важен — лимиты, логи, метрики — считай байты: сервер и сеть живут в них, а не в символах.

Первый взгляд на разбор

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

одна строка - и текст стал данными
import json

API_RESPONSE = '{"city": "Москва", "temp": 21}'

data = json.loads(API_RESPONSE)
print(data["city"], data["temp"])
Вывод
Москва 21
ошибка нетерпеливого
API_RESPONSE = '{"city": "Москва", "temp": 21}'

try:
    print(API_RESPONSE["city"])
except TypeError as e:
    print("TypeError:", e)
Вывод
TypeError: string indices must be integers, not 'str'

Пайплайн словами

Соберём путь ответа в четыре шага — ровно этот пайплайн повторяет любая библиотека, от requests до httpx. Запрос уходит по URL. Сервер возвращает статус и тело-байты. Байты декодируются в текст. Текст отдаётся тебе как .text, а после json.loads — как .json(). Всё остальное — удобства вокруг этих четырёх шагов.

так это выглядит на своём компьютере
# На своём компьютере, после pip install requests:
import requests

r = requests.get("https://api.weather.example/v1/weather",
                 params={"city": "Москва"})
print(r.status_code)      # 200 - статус разговора
print(r.text)             # тело ответа - обычная строка с JSON

data = r.json()           # готовый разбор: то же, что json.loads(r.text)
Блок для запуска на своём компьютере: в песочнице сети нет. Каждая строка пайплайна здесь уже отработана на сохранённом ответе — не запускается только сама отправка.

Заметь, что r.json() и json.loads(r.text) — одно и то же действие: библиотека просто прячет его за удобным методом. Понимая пайплайн, ты читаешь любую HTTP-библиотеку как открытую книгу — и не падаешь, когда удобный метод молча возвращает строку, потому что сервер прислал не JSON.

Заголовки — маленький словарь метаданных: в каком формате тело, когда ответ родился, кто сервер. Библиотеки складывают их в обычный словарь, и работают с ними словарные приёмы: доступ по имени, get с умолчанием для необязательных:

заголовки как словарь
API_HEADERS = {
    "content-type": "application/json; charset=utf-8",
    "server": "nginx/1.24",
    "date": "Mon, 06 Oct 2026 10:00:00 GMT",
}

print(len(API_HEADERS))
print(API_HEADERS["server"])
print(API_HEADERS.get("x-request-id", "нет такого заголовка"))
Вывод
3
nginx/1.24
нет такого заголовка

get с умолчанием здесь особенно уместен: заголовки приходят не все и не всегда — кто-то из прокси что-то отрезал, кто-то из версий сервера ещё не знает нового поля. Чтение заголовка скобками — способ уронить скрипт на честном ответе, у которого просто не оказалось нужной строчки.

Сводка по сохранённому ответу

Соберём знакомство с ответом: статус, формат из заголовка, длина тела и само тело. Такой мини-отчёт удобно печатать при отладке любого запроса.

сводка по ответу
API_STATUS = 200
API_HEADERS = {"content-type": "application/json; charset=utf-8"}
API_RESPONSE = '{"city": "Москва", "temp": 21, "sky": "облачно"}'

print("Статус:", API_STATUS)
print("Формат:", API_HEADERS["content-type"].split(";")[0])
print("Длина тела:", len(API_RESPONSE), "символов")
print("Тело:", API_RESPONSE)
Вывод
Статус: 200
Формат: application/json
Длина тела: 48 символов
Тело: {"city": "Москва", "temp": 21, "sky": "облачно"}

Заголовок content-type мы разрезали методом split(";")[0] — до точки с запятой: левая часть и есть формат, правая — кодировка. Если сервер честно назвал себя application/json, тело можно смело нести в json.loads из следующего урока.

Сегодня мы разобрали конверт: статус, заголовки, текст, байты. Завтрашняя почта — содержимое: урок 14 превращает текст в словарь и достаёт из него поля с проверками. А если разбор всё-таки падает — вспоминай урок 9 про битый JSON: там уже лежат все инструменты для этого случая.

Конверт ответа открывается в порядке: статус, заголовки, тело — и только после json.loads текст становится данными.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

BODY = '{"a": 1}'
print(type(BODY).__name__, len(BODY))
STATUS = 404
if STATUS == 200:
    print("ok")
else:
    print("fail", STATUS)
RAW = b" temp "
print(RAW.decode("utf-8").strip(), type(RAW).__name__)
Проверь себя
0 / 5

1. Что такое тело HTTP-ответа?

2. Что означает статус 200?

3. Что возвращает r.text в requests?

4. Что вернёт len(API_RESPONSE), если API_RESPONSE — тело-строка?

5. Что произойдёт при API_RESPONSE["city"], если API_RESPONSE — строка с JSON?

Карточки терминов
Запомнено: 0 / 6
Практика

Дан сохранённый ответ API со статусом 200. Выведи три строки: статус («Статус: …»), количество символов в теле («Символов: …» через len) и результат проверки, что в теле есть фрагмент с кавычками вокруг temp («Есть temp: …» через in).

practice.py
Вопросы и ответы по уроку

Что возвращает API — JSON или текст?

Текст, внутри которого записан JSON. Пока текст не прошёл json.loads, это строка без ключей: у неё есть длина и срезы, но нет полей. Разбор в словарь — отдельный шаг, о котором следующий урок.

Как понять, что запрос к API удался?

По статус-коду: 200 — успех. Проверяй статус до разбора тела: на 404 и 500 тело может быть текстом ошибки или HTML, и json.loads на нём упадёт. Ошибочный ответ тоже можно разобрать — в нём обычно есть поле error с описанием.

Почему в песочнице самоучителя нельзя сделать настоящий запрос?

Код исполняется в браузере, а страница не имеет доступа к сети. Это честное ограничение платформы: всё, что не требует сети — статусы, тексты, байты, разборы, — исполняется на странице по-настоящему. Сам запрос делается на своём компьютере: requests.get или urlopen.

Что делать с байтами из ответа сервера?

Декодировать в строку с указанием кодировки из заголовка Content-Type: body.decode("utf-8"). У байтов нет ключей и букв — только числа; текст появляется после decode. Неверная кодировка — источник знаменитых кракозябр вместо кириллицы.

Зачем читать заголовки ответа, если тело уже есть?

Заголовки рассказывают, что за тело: Content-Type называет формат и кодировку, Date — время сервера, X-заголовки — служебные метки прокси. Формат из Content-Type — подсказка, скармливать ли тело json.loads сразу или сначала глянуть глазами.

Понравился урок? Сошлитесь на него

«Ответ API — это сначала текст со статусом, и только после json.loads — данные, с которыми можно работать.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.