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 — только теперь вы знаете, что он делает за вас.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
answer.read()даёт байты и откуда брать кодировку дляdecode? - Чем
HTTPErrorотличается отURLErrorи почему порядок веток важен? - Зачем запросу
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.
Куда это встанет в проекте
У сводки появляется второй источник. Инфляция приходила из Всемирного банка раз в год; курс валют Нацбанк отдаёт каждый рабочий день, и сводка впервые получает данные, у которых есть «сегодня».
Долги. Мы вытащили курс из разметки строковыми методами — так нельзя, и следующий урок это чинит. И мы ходим в сеть при каждом запуске: кэш на диске у нас есть с одиннадцатого урока, а вот привязать его к дате ещё предстоит.
Ответы
Показать ответы
На вопросы
- Потому что по сети идут байты, а не текст: у HTTP нет обязанности знать, что внутри. Кодировку называет сам сервер — в заголовке
Content-Type; берут её оттуда, а на случай, если сервер промолчал, договариваются об умолчании. HTTPError— сервер ответил, но отказом: у него есть код (404,403,500).URLError— ответа не было вовсе: нет сети, не разрешилось имя. Первое — частный случай второго, поэтому ловить его надо раньше, иначе общая ветка заберёт всё.- Потому что сервер не обязан отвечать. Без
timeoutпрограмма ждёт столько, сколько он молчит, и со стороны это выглядит как зависание — а не как «сеть сегодня плохая».
К разминке
bytes 21иstr <?xml. По сети приходят байты;decodeделает из них строку, и только у строки есть привычные буквы.
bytes 21
str <?xml
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
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
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.