Что отдаёт API: ответ как текст
API отвечает текстом со статусом и заголовками: берём заранее сохранённый настоящий ответ — и учимся мерить его длину, искать подстроки и превращать байты в строку.
Редакция Питоники
Восемь уроков мы работали с JSON, который кто-то любезно положил нам в переменную. Пора посмотреть, откуда он берётся. Запрос к API по URL из прошлого урока возвращает HTTP-ответ — а это не словарь и не «данные», это конверт: строка статуса, блок заголовков и тело, в котором JSON приходит обычным текстом. Сегодня разбираем конверт. Ответ API — это сначала текст со статусом, и только после json.loads — данные, с которыми можно работать.
Одно честное уточнение сразу. Браузерная песочница, в которой исполняется код на странице, не имеет доступа к сети — настоящий запрос из неё не отправить. Поэтому работать будем с заранее сохранённым настоящим ответом: текст, который сервер отдал на реальный запрос, записан в константу. Каждая буква в нём настоящая — просто конверт доставлен заранее.
Как устроен сам разговор? Ты — клиент: отправляешь запрос с адресом и хочешь данные. Сервер — программа на чужой машине: читает запрос, собирает ответ и возвращает его. Один такой обмен — запрос туда, ответ обратно — и есть работа любой HTTP-библиотеки. Всё остальное — страницы, мобильные приложения, боты — стоит на этом простом обмене сообщениями.
Сохранённый ответ: это строка
Вот ответ сервиса погоды, сохранённый в константу так же, как его сохраняет в .text библиотека requests. Проверим тип и померим длину — как для любой строки:
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__)
1. Что такое тело HTTP-ответа?
2. Что означает статус 200?
3. Что возвращает r.text в requests?
4. Что вернёт len(API_RESPONSE), если API_RESPONSE — тело-строка?
5. Что произойдёт при API_RESPONSE["city"], если API_RESPONSE — строка с JSON?
Дан сохранённый ответ API со статусом 200. Выведи три строки: статус («Статус: …»), количество символов в теле («Символов: …» через len) и результат проверки, что в теле есть фрагмент с кавычками вокруг temp («Есть temp: …» через in).
Что возвращает 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
json · Урок 12
Собираем URL для API: urllib.parse
Схема, хост, путь, query: urlparse разбирает адрес, urlencode кодирует кириллицу в проценты, parse_qs читает строку запроса обратно — и весь запрос к API собирается без сети.
json · Урок 9
Битый JSON: JSONDecodeError и как его читать
Одинарные кавычки, висячая запятая, True вместо true — три классические причины JSONDecodeError, и текст ошибки, который честно называет строку и колонку.
json · Урок 14
json.loads на ответе API: от текста к данным
Текст, json.loads, словарь, поля: собираем полный конвейер разбора ответа API — с проверкой структуры, обработкой пустого и битого тела.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
requests · Урок 4
Ответ requests: text, content и json — три лица ответа
У ответа requests три лица: .text — строка, .content — байты, .json() — готовый объект Python. Разбираемся, когда какое брать и откуда берутся кракозябры.
requests text contentresponse.json python
requests · Урок 1
Библиотека requests Python с нуля: первый GET-запрос
Первый GET-запрос на requests: что улетает по сети, что возвращается и как разобрать ответ на статус, заголовки и тело — механику проверяем прямо в браузере.
python http запросrequests response text
json · Урок 7
Вложенные структуры: списки в словарях и наоборот
Настоящие данные не плоские: список товаров внутри заказа, массив объектов в ответе API. Учимся читать вложенный JSON сверху вниз и не теряться в глубине.
вложенный json pythonjson список словарей