
HTTP-запросы в Python: urllib без сторонних библиотек
Отправляем GET-запрос с тайм-аутом, читаем статус, заголовки и ограниченное тело ответа, а затем различаем HTTP-ошибку и сетевой сбой.
Содержание
Скрипту мониторинга или импорта часто нужен один внешний документ: отправить запрос, убедиться в статусе и разобрать тело. Для этого достаточно стандартного пакета urllib. Он подробней сторонних высокоуровневых клиентов, зато уже входит в Python и явно показывает части HTTP-обмена.
Запрос содержит метод, адрес и заголовки. Ответ содержит числовой статус, заголовки и тело. Статус сообщает результат на уровне HTTP, заголовки описывают ответ, а тело несёт HTML, JSON или другой формат. Успешное соединение ещё не означает, что получены нужные данные.
Этот урок использует обработку исключений и подготовит сетевую функцию для итогового мониторинга доступности.
Отправьте GET-запрос с явным тайм-аутом
Создайте локальный файл http_demo.py. Код отправляет один GET-запрос на демонстрационный домен, печатает метаданные и читает не более 4096 байт:
from urllib.request import Request, urlopen
url = "https://example.com/"
request = Request(
url,
headers={"User-Agent": "learning-http-client/1.0"},
method="GET",
)
with urlopen(request, timeout=5) as response:
body = response.read(4096)
print(f"Статус: {response.status}")
print(f"Финальный URL: {response.url}")
print(f"Тип данных: {response.headers.get_content_type()}")
print(f"Получено байт: {len(body)}")
Запустите python http_demo.py на компьютере с доступом в интернет. Для https://example.com/ ожидается успешный статус и тип text/html, но сеть и удалённый сервер находятся вне контроля программы. Важно, что скрипт либо получает проверяемый ответ, либо завершается исключением, а не ждёт бесконечно.
timeout=5 ограничивает блокирующие сетевые операции в секундах. Он не ограничивает размер ответа, поэтому read(4096) задаёт отдельный предел. Если программе нужен весь документ, сначала проверьте ожидаемый Content-Type и предусмотрите максимально допустимый размер.
Декодируйте текст по объявленной кодировке
Метод read возвращает байты. Чтобы получить строку, нужна кодировка. Расширьте код внутри блока with после чтения:
charset = response.headers.get_content_charset() or "utf-8"
text = body.decode(charset)
print(text[:200])
Сервер может указать кодировку в Content-Type; если её нет, пример использует UTF-8. Первые 200 символов нужны только для учебной проверки и не изменяют ответ.
Не применяйте errors="ignore" по умолчанию. Молчаливое удаление неподходящих байтов может повредить JSON, имя или подпись. Ошибка декодирования должна привести к понятному сообщению либо к заранее оговорённой альтернативной кодировке.
Различайте ответ с ошибкой и отсутствие ответа
HTTPError содержит HTTP-статус, например 404 или 503. URLError охватывает более ранние проблемы: имя не разрешилось, соединение отклонено, TLS не прошёл проверку или сработал тайм-аут. Замените простой вызов полной функцией:
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def fetch(url, timeout=5, max_bytes=65536):
request = Request(
url,
headers={"User-Agent": "learning-http-client/1.0"},
method="GET",
)
try:
with urlopen(request, timeout=timeout) as response:
status = response.status
content_type = response.headers.get_content_type()
body = response.read(max_bytes + 1)
except HTTPError as error:
raise RuntimeError(
f"Сервер вернул HTTP {error.code} для {url}"
) from error
except URLError as error:
raise RuntimeError(
f"Не удалось получить ответ от {url}: {error.reason}"
) from error
if len(body) > max_bytes:
raise ValueError(f"Ответ превышает предел {max_bytes} байт")
return status, content_type, body
HTTPError перехватывается первым, потому что он является подклассом URLError. Функция читает на один байт больше лимита: так она отличает документ ровно допустимого размера от обрезанного. Возвращаемые данные ещё не декодированы — вызывающий код сам решит, ждать ли HTML, JSON или двоичный файл.
Добавьте безопасный вызов в конец файла:
if __name__ == "__main__":
try:
status, content_type, body = fetch("https://example.com/")
except (RuntimeError, ValueError) as error:
print(f"Ошибка: {error}")
raise SystemExit(1)
print(status, content_type, len(body))
При успешном ответе процесс завершится с кодом 0; при обработанной ошибке — с кодом 1. Функция ничего не записывает на диск. Чтобы проверить HTTP-ветку, временно укажите адрес заведомо отсутствующей страницы на собственном тестовом сервере и убедитесь, что сообщение содержит статус.
Проверяйте тип перед разбором JSON
Если ожидается JSON, сначала проверьте тип, затем декодируйте и вызовите json.loads. Добавьте этот фрагмент после fetch только для API, контракт которого вам известен:
import json
status, content_type, body = fetch("https://api.example.test/data")
if content_type != "application/json":
raise ValueError(f"Ожидался JSON, получен {content_type}")
data = json.loads(body.decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("В корне ответа должен быть объект JSON")
Адрес api.example.test является примером и не предназначен для запуска. Подставьте документированный тестовый адрес API своей системы. Как и при чтении локального JSON, успешный синтаксический разбор не подтверждает наличие обязательных полей.
Не отключайте проверку TLS ради удобства
Для HTTPS стандартный контекст проверяет сертификат и имя сервера. Не создавайте непроверяющий контекст с CERT_NONE: он превращает защищённое соединение в уязвимое для подмены. Если внутренний сервис использует собственный центр сертификации, добавьте его сертификат в доверенный контекст явно.
Автоматические перенаправления тоже меняют конечный адрес. Сравнивайте response.url с ожидаемым доменом, если данные или заголовок авторизации нельзя отправлять на другой узел. Не принимайте произвольный пользовательский URL в серверном приложении без отдельного ограничения схемы и адресов.
Официальная документация описывает urllib.request и исключения urllib.error. Следующий урок уберёт токены и изменяемые настройки из исходного кода с помощью переменных окружения.
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Нужно ли устанавливать urllib через pip?
Нет. Пакет urllib входит в стандартную библиотеку Python. Для более сложного клиента можно выбрать стороннюю библиотеку, но простой GET-запрос не требует зависимости.
Чем HTTPError отличается от URLError?
HTTPError означает, что сервер прислал HTTP-ответ с ошибочным статусом, например 404. URLError обычно описывает проблему до получения ответа: DNS, соединение, TLS или тайм-аут.
Зачем ограничивать объём читаемого ответа?
Удалённый сервер может вернуть неожиданно большой поток. Ограничение защищает память и показывает, какой объём действительно нужен программе.


