Shanraq.org Shanraq.org
HTTP из Python: статус, байты и два разных «не получилось»
IT

Python: от данных до своей сводки Урок 18 из 56

HTTP из Python: статус, байты и два разных «не получилось»

Восемнадцатый урок курса по Python. Запрос к Нацбанку: 200, `text/xml`, 11 407 байт — и это `bytes`, а не строка. Плюс разница, из-за которой рушится обработка ошибок: `HTTPError` — сервер ответил «нет», `URLError` — ответа не было вовсе.

Зачем это нужно

Первый урок уже ходил в сеть — и честно предупредил: «запустите, ничего не понимая». Сегодня понимаем.

Данные, которые стоит считать, почти всегда лежат у кого-то другого: у банка, у статистики, у погоды. Между вашей программой и ими — HTTP: адрес, запрос, ответ.

Разбираем его на курсах Нацбанка, потому что это самый близкий пример из тех, что отдают числа без ключа и без регистрации.

Сразу целиком

Файл surau.py. Запуск: python surau.py из окружения.

Обязательное — первый блок: запрос, статус, байты, кодировка. Второй показывает, откуда берётся дата в адресе, третий — два вида неудачи.

День в уроке закреплён нарочно: курс за вчера меняется каждую ночь, а урок обязан печатать то, что у вас получится. Как взять именно вчера — сказано в самом уроке.

"""Урок 18: HTTP — как программа спрашивает чужой сервер.

Первый урок уже ходил в сеть, ничего не объясняя. Сегодня разбираем сам поход:
адрес, заголовки, статус, байты — и что делать, когда сервер отвечает «нет».
"""

import urllib.error
import urllib.request
from datetime import date, timedelta

# Нацбанк отдаёт официальные курсы за один день. Дата стоит прямо в адресе,
# в виде день.месяц.год. В уроке день закреплён: так вывод одинаков у всех.
TEMPLATE = "https://nationalbank.kz/rss/get_rates.cfm?fdate={}"
URL = TEMPLATE.format("15.01.2026")

print("== запрос и ответ")
request = urllib.request.Request(URL, headers={"User-Agent": "shanraq-course/1.0"})
with urllib.request.urlopen(request, timeout=30) as answer:
    status = answer.status
    kind = answer.headers.get_content_type()
    charset = answer.headers.get_content_charset()
    body = answer.read()

print("статус:", status)
print("тип содержимого:", kind, "| кодировка:", charset)
print("получено:", len(body), "байт, тип данных:", type(body).__name__)

text = body.decode(charset or "utf-8")
print("первая строка:", text.splitlines()[0])
print("дата в ответе:", text.split("<date>")[1].split("</date>")[0])

print()
print("== «вчера» — это дата, а не слово")
day = date(2026, 1, 15)
print("адрес на этот день:", TEMPLATE.format(day.strftime("%d.%m.%Y")))
print("вчера считают так:  date.today() -", repr(timedelta(days=1)))

print()
print("== когда сервер отвечает «нет»")
try:
    urllib.request.urlopen("https://nationalbank.kz/net-takogo-adresa", timeout=30)
except urllib.error.HTTPError as error:
    print("HTTPError — сервер ответил:", error.code, error.reason)

try:
    urllib.request.urlopen("https://net-takogo-servera.shanraq.invalid", timeout=10)
except urllib.error.URLError as error:
    print("URLError — ответа не было:", type(error.reason).__name__)

Выводит:

== запрос и ответ
статус: 200
тип содержимого: text/xml | кодировка: utf-8
получено: 11407 байт, тип данных: bytes
первая строка: <?xml version="1.0" encoding="utf-8"?>
дата в ответе: 15.01.2026

== «вчера» — это дата, а не слово
адрес на этот день: https://nationalbank.kz/rss/get_rates.cfm?fdate=15.01.2026
вчера считают так:  date.today() - datetime.timedelta(days=1)

== когда сервер отвечает «нет»
HTTPError — сервер ответил: 404 Not Found
URLError — ответа не было: gaierror

Разбор

Ответ приходит байтами, а не текстом

получено: 11407 байт, тип данных: bytes

answer.read() даёт bytes — сырые байты, как они пришли по сети. Текстом их делает decode, и кодировку для этого не выдумывают: сервер называет её сам, в заголовке Content-Type.

charset = answer.headers.get_content_charset()
text = body.decode(charset or "utf-8")

or "utf-8" здесь не лень, а решение: не всякий сервер называет кодировку, и надо заранее знать, что делать в этом случае. Это тот же разговор, что в одиннадцатом уроке про файлы, только договор о байтах теперь приходит вместе с ними.

Образ. Посылка. На ленте приезжает коробка, а не вещь: чтобы получить вещь, коробку надо открыть тем ключом, который написан на наклейке.

Статус — первое, что читают

статус: 200

200 значит «вот ответ». Ещё вы встретите 301 и 302 (переехало, urllib пойдёт следом сам), 404 (адреса нет), 403 (нельзя), 429 (слишком часто спрашиваете), 500 (сломалось у них).

Правило простое: сначала статус, потом тело. Тело у ошибки тоже бывает — страница «не найдено» тоже страница, — и разбор её как данных даёт самые странные ошибки на свете.

Заголовки: кто спрашивает и сколько ждать

request = urllib.request.Request(URL, headers={"User-Agent": "shanraq-course/1.0"})
with urllib.request.urlopen(request, timeout=30) as answer:

User-Agent — подпись программы. По умолчанию urllib подписывается собой, и часть серверов такие запросы отклоняет; назвать себя честно — и вежливее, и надёжнее.

timeout=30 обязателен. Без него программа может ждать ответа вечно: сервер не обязан ни отвечать, ни закрывать соединение. Пять минут молчания без таймаута выглядят как «программа зависла».

with закрывает соединение так же, как закрывал файл: даже если внутри случилась ошибка.

Два разных «не получилось»

HTTPError — сервер ответил: 404 Not Found
URLError — ответа не было: gaierror

Это разные беды, и лечатся они по-разному. HTTPError значит, что сервер жив и ответил — просто ответил «нет»: адрес не тот, доступ закрыт, слишком часто спрашиваете. Тут смотрят error.code.

URLError значит, что ответа не было вообще: нет сети, имя не разрешилось, соединение оборвалось. Внутри лежит причина (error.reason), и по её типу видно, что именно случилось.

Важная тонкость: HTTPErrorчастный случай URLError. Значит, порядок веток решает, как в десятом уроке: частное выше общего, иначе except URLError заберёт всё и код ответа вы не увидите. На этом и построена третья разминка.

requests — то же самое, но не из коробки

В чужом коде вы почти всегда увидите не urllib, а requests:

# то же самое на requests — его ставят отдельно: pip install requests
import requests

answer = requests.get(URL, timeout=30, headers={"User-Agent": "shanraq-course/1.0"})
print(answer.status_code, answer.headers["content-type"])
print(answer.text[:38])          # decode сделан за вас

Он короче и удобнее: сам разбирает кодировку, сам держит сессии. Но он не приходит с Python — его надо ставить, а значит, у программы появляется зависимость. Курс держится стандартной библиотеки, чтобы всё запускалось везде; в своей работе берите requests — только теперь вы знаете, что он делает за вас.

Карта урока

Карта урока: адрес, статус и байты

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему answer.read() даёт байты и откуда брать кодировку для decode?
  2. Чем HTTPError отличается от URLError и почему порядок веток важен?
  3. Зачем запросу timeout, если сеть и так быстрая?

Разминка

Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает эта программа?

body = b'<?xml version="1.0"?>'
print(type(body).__name__, len(body))
text = body.decode("utf-8")
print(type(text).__name__, text[:5])

2. Заполните пропуск. Вместо ... поставьте то, что даст дату в виде 15.01.2026.

from datetime import date

TEMPLATE = "https://nationalbank.kz/rss/get_rates.cfm?fdate={}"
day = date(2026, 1, 15)
print(TEMPLATE.format(...))

3. Почините. Сервер ответил 404, а программа говорит про недоступную сеть. Найдите причину — она в порядке веток.

import urllib.error

try:
    raise urllib.error.HTTPError("url", 404, "Not Found", None, None)
except urllib.error.URLError as error:
    print("сеть недоступна:", error.reason)
except urllib.error.HTTPError as error:
    print("код ответа:", error.code)

Задание

Обязательное. Заберите у Нацбанка курс доллара за закреплённый день — 15.01.2026:

TEMPLATE = "https://nationalbank.kz/rss/get_rates.cfm?fdate={}"

Запрос подпишите своим User-Agent и поставьте timeout. Прочитайте статус и число полученных байт, раскодируйте тело кодировкой, которую назвал сервер, и напечатайте дату из ответа. Затем достаньте курс доллара и его изменение за день — пока строковыми методами, разбор разметки будет в следующем уроке. Ответ сервера с ошибкой обработайте отдельной веткой.

Ожидаемый вывод:

статус: 200 | получено байт: 11407
дата в ответе: 15.01.2026
доллар: 510.43 тенге | изменение за день: +0.68

Готово, когда: вывод совпадает построчно; у запроса есть и timeout, и свой User-Agent; кодировка взята из заголовка ответа, а не вписана руками; HTTPError перехвачен раньше URLError.

На своих данных. Возьмите вчерашнюю дату — date.today() - timedelta(days=1) — и запросите курс за неё. Число будет другим у каждого читателя и в каждый день: это и есть разница между уроком, который печатает измеренное, и программой, которая живёт в настоящем времени.

По желанию.

  • Запросите день, которого ещё не было, и посмотрите, что придёт вместо чисел.
  • Уберите User-Agent и сравните ответ.
  • Поставьте timeout=0.001 и прочитайте, чем ответит urlopen.

Куда это встанет в проекте

У сводки появляется второй источник. Инфляция приходила из Всемирного банка раз в год; курс валют Нацбанк отдаёт каждый рабочий день, и сводка впервые получает данные, у которых есть «сегодня».

Долги. Мы вытащили курс из разметки строковыми методами — так нельзя, и следующий урок это чинит. И мы ходим в сеть при каждом запуске: кэш на диске у нас есть с одиннадцатого урока, а вот привязать его к дате ещё предстоит.

Ответы

Показать ответы

На вопросы

  1. Потому что по сети идут байты, а не текст: у HTTP нет обязанности знать, что внутри. Кодировку называет сам сервер — в заголовке Content-Type; берут её оттуда, а на случай, если сервер промолчал, договариваются об умолчании.
  2. HTTPError — сервер ответил, но отказом: у него есть код (404, 403, 500). URLError — ответа не было вовсе: нет сети, не разрешилось имя. Первое — частный случай второго, поэтому ловить его надо раньше, иначе общая ветка заберёт всё.
  3. Потому что сервер не обязан отвечать. Без timeout программа ждёт столько, сколько он молчит, и со стороны это выглядит как зависание — а не как «сеть сегодня плохая».

К разминке

  1. bytes 21 и str <?xml. По сети приходят байты; decode делает из них строку, и только у строки есть привычные буквы.
bytes 21
str <?xml
  1. day.strftime("%d.%m.%Y"). Нацбанк ждёт дату в виде день.месяц.год, и это ровно тот формат, который в четырнадцатом уроке читался двумя способами: здесь его называют явно.
from datetime import date

TEMPLATE = "https://nationalbank.kz/rss/get_rates.cfm?fdate={}"
day = date(2026, 1, 15)
print(TEMPLATE.format(day.strftime("%d.%m.%Y")))
https://nationalbank.kz/rss/get_rates.cfm?fdate=15.01.2026
  1. HTTPError — частный случай URLError, а частное в цепочке except ставят выше общего. Иначе первая же ветка забирает всё, и код ответа теряется:
import urllib.error

try:
    raise urllib.error.HTTPError("url", 404, "Not Found", None, None)
except urllib.error.HTTPError as error:
    print("код ответа:", error.code, error.reason)
except urllib.error.URLError as error:
    print("сеть недоступна:", error.reason)
код ответа: 404 Not Found

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.