Клиентский модуль отправляет HTTP-запрос и получает статус и данные ответа
Программирование

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. Следующий урок уберёт токены и изменяемые настройки из исходного кода с помощью переменных окружения.

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Что ограничивает аргумент timeout в urlopen?
02Какой объект содержит метод, URL и заголовки запроса?
03Почему HTTPError перехватывают раньше URLError?

Разбираем коротко

Частые вопросы

Нужно ли устанавливать urllib через pip?

Нет. Пакет urllib входит в стандартную библиотеку Python. Для более сложного клиента можно выбрать стороннюю библиотеку, но простой GET-запрос не требует зависимости.

Чем HTTPError отличается от URLError?

HTTPError означает, что сервер прислал HTTP-ответ с ошибочным статусом, например 404. URLError обычно описывает проблему до получения ответа: DNS, соединение, TLS или тайм-аут.

Зачем ограничивать объём читаемого ответа?

Удалённый сервер может вернуть неожиданно большой поток. Ограничение защищает память и показывает, какой объём действительно нужен программе.