Python requests время ожидания

Использование requests в Python — тайм-ауты, повторы, хуки

HTTP-библиотека Python requests — это, вероятно, моя любимая HTTP-утилита во всех языках, на которых я программирую. Она проста, интуитивно понятна и повсеместно распространена в сообществе Python. Большинство программ, взаимодействующих с HTTP, используют либо requests, либо urllib3 из стандартной библиотеки.

Хотя благодаря простому API можно сразу же начать продуктивно работать с запросами, библиотека также предлагает расширяемость для продвинутых случаев использования. Если вы пишете API-клиент или веб-скребок, вам наверняка понадобится устойчивость к сетевым сбоям, полезные трассировки отладки и синтаксический сахар

Ниже приводится краткое описание возможностей, которые я нашел полезными в запросах при написании инструментов веб-скрейпинга или программ, которые широко используют JSON API.

Хуки для запросов

Часто при использовании стороннего API вы хотите убедиться, что возвращаемый ответ действительно действителен. Requests предлагает сокращенный помощник raise_for_status() , который утверждает, что код состояния HTTP ответа не является 4xx или 5xx, т.е. что запрос не привел к ошибке клиента или сервера.

response = requests.get('https://api.github.com/user/repos?page=1') # Assert that there were no errors response.raise_for_status()

Это может стать повторяющимся, если вам нужно запрашивать raise_for_status() для каждого вызова. К счастью, библиотека requests предлагает интерфейс ‘hooks’, с помощью которого вы можете подключить обратные вызовы на определенные части процесса запроса.

Мы можем использовать хуки, чтобы обеспечить вызов raise_for_status() для каждого объекта ответа.

# Create a custom requests object, modifying the global module throws an error http = requests.Session() assert_status_hook = lambda response, *args, **kwargs: response.raise_for_status() http.hooks["response"] = [assert_status_hook] http.get("https://api.github.com/user/repos?page=1") > HTTPError: 401 Client Error: Unauthorized for url: https://api.github.com/user/repos?page=1

Установка базовых URL

Предположим, что вы используете только один API, размещенный на api.org. В итоге вы будете повторять протокол и домен для каждого вызова http:

requests.get('https://api.org/list/') requests.get('https://api.org/list/3/item')

Вы можете сэкономить на вводе текста, используя BaseUrlSession. Это позволит вам указать базовый url для HTTP клиента и указать только путь к ресурсу во время запроса.

from requests_toolbelt import sessions http = sessions.BaseUrlSession(base_url="https://api.org") http.get("/list") http.get("/list/item")

Обратите внимание, что набор инструментов requests toolbelt не включен в стандартную установку requests, поэтому вам придется установить его отдельно.

Установка тайм-аутов по умолчанию

Документация по requests рекомендует устанавливать таймауты для всего производственного кода. Если вы забудете установить тайм-ауты, неправильно работающий сервер может привести к зависанию вашего приложения, особенно учитывая, что большинство кода Python является синхронным.

requests.get('https://github.com/', timeout=0.001)

Используя Транспортные адаптеры, мы можем установить таймаут по умолчанию для всех HTTP-вызовов. Это гарантирует, что будет установлен разумный тайм-аут, даже если разработчик забудет добавить параметр timeout=1 в свой отдельный вызов, но позволяет отменить его на основе каждого вызова.

Читайте также:  Javascript find parent by class

Ниже приведен пример пользовательского транспортного адаптера с таймаутами по умолчанию, вдохновленный этим комментарием на Github. Мы переопределяем конструктор для предоставления таймаута по умолчанию при создании http-клиента и метод send() для обеспечения использования таймаута по умолчанию, если не предоставлен аргумент таймаута.

from requests.adapters import HTTPAdapter DEFAULT_TIMEOUT = 5 # seconds class TimeoutHTTPAdapter(HTTPAdapter): def __init__(self, *args, **kwargs): self.timeout = DEFAULT_TIMEOUT if "timeout" in kwargs: self.timeout = kwargs["timeout"] del kwargs["timeout"] super().__init__(*args, **kwargs) def send(self, request, **kwargs): timeout = kwargs.get("timeout") if timeout is None: kwargs["timeout"] = self.timeout return super().send(request, **kwargs)

Мы можем использовать его следующим образом:

import requests http = requests.Session() # Mount it for both http and https usage adapter = TimeoutHTTPAdapter(timeout=2.5) http.mount("https://", adapter) http.mount("http://", adapter) # Use the default 2.5s timeout response = http.get("https://api.twilio.com/") # Override the timeout as usual for specific requests response = http.get("https://api.twilio.com/", timeout=10)

Причина отказа

Сетевые соединения несут потери, перегружены, а серверы выходят из строя. Если мы хотим создать действительно надежную программу, мы должны учитывать отказы и иметь стратегию повторных попыток.

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

from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry retry_strategy = Retry( total=3, status_forcelist=[429, 500, 502, 503, 504], method_whitelist=["HEAD", "GET", "OPTIONS"] ) adapter = HTTPAdapter(max_retries=retry_strategy) http = requests.Session() http.mount("https://", adapter) http.mount("http://", adapter) response = http.get("https://en.wikipedia.org/w/api.php")

Класс Retry по умолчанию предлагает разумные параметры по умолчанию, но в значительной степени настраивается, поэтому здесь приводится сводка наиболее часто используемых мною параметров.

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

Общее количество повторных попыток. Если количество неудачных запросов или перенаправлений превысит это число, клиент выбросит исключение urllib3.exceptions.MaxRetryError . Я варьирую этот параметр в зависимости от API, с которым работаю, но обычно устанавливаю его меньше 10, обычно достаточно 3 повторных попыток.

status_forcelist=[413, 429, 503]

Коды ответов HTTP для повторных попыток. Скорее всего, вы захотите повторить попытку при обычных ошибках сервера (500, 502, 503, 504), поскольку серверы и обратные прокси не всегда придерживаются спецификации HTTP. Всегда повторяйте попытку при превышении лимита скорости 429, потому что библиотека urllib по умолчанию должна инкрементально отступать при неудачных запросах.

method_whitelist=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]

Методы HTTP для повторных попыток. По умолчанию сюда включены все HTTP-методы, кроме POST, поскольку POST может привести к новой вставке. Измените этот параметр так, чтобы он включал POST, потому что большинство API, с которыми я имею дело, не возвращают код ошибки и не выполняют вставку в одном и том же вызове. А если они это делают, вам, вероятно, следует опубликовать сообщение об ошибке.

Читайте также:  Opencv find rectangle python

Это интересный параметр. Он позволяет изменять время сна процессов между неудачными запросами. Алгоритм следующий:

Например, если коэффициент отката установлен на:

  • 1 секунда последующие сны будут 0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256 .
  • 2 секунды — 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
  • 10 секунд — 5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560

Значение экспоненциально возрастает, что является разумной реализацией по умолчанию для стратегий возврата.

По умолчанию это значение равно 0, что означает, что экспоненциальный откат не будет установлен и повторные попытки будут выполняться немедленно. Обязательно установите значение 1, чтобы избежать забивания ваших серверов!.

Полная документация по модулю retry находится здесь.

Комбинирование тайм-аутов и повторных попыток

Поскольку HTTPAdapter является сравнимым, мы можем объединить повторные попытки и таймауты следующим образом:

retries = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) http.mount("https://", TimeoutHTTPAdapter(max_retries=retries))

Отладка HTTP-запросов

Иногда запросы не работают, и вы не можете понять, почему. Логирование запроса и ответа может дать вам понимание причины неудачи. Есть два способа сделать это — либо используя встроенные настройки отладочного протоколирования, либо используя крючки запросов.

Печать HTTP-заголовков

При изменении уровня отладки логирования больше 0 будут записываться HTTP-заголовки ответа. Это самый простой вариант, но он не позволяет вам увидеть HTTP-запрос или тело ответа. Это полезно, если вы имеете дело с API, который возвращает большое тело полезной нагрузки, не подходящее для протоколирования, или содержит двоичное содержимое.

Любое значение, которое больше 0, включит ведение отладочного журнала.

import requests import http http.client.HTTPConnection.debuglevel = 1 requests.get("https://www.google.com/") # Output send: b'GET / HTTP/1.1\r\nHost: www.google.com\r\nUser-Agent: python-requests/2.22.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n' reply: 'HTTP/1.1 200 OK\r\n' header: Date: Fri, 28 Feb 2020 12:13:26 GMT header: Expires: -1 header: Cache-Control: private, max-age=0

Печать всего

Если вы хотите регистрировать весь жизненный цикл HTTP, включая текстовое представление запроса и ответа, вы можете использовать крючки запроса и утилиты дампа из requests_toolbelt.

Читайте также:  Html center div vertically

Я предпочитаю этот вариант всегда, когда имею дело с API на основе REST, который не возвращает очень большие ответы.

import requests from requests_toolbelt.utils import dump def logging_hook(response, *args, **kwargs): data = dump.dump_all(response) print(data.decode('utf-8')) http = requests.Session() http.hooks["response"] = [logging_hook] http.get("https://api.openaq.org/v1/cities", params=) # Output < GET /v1/cities?country=BA HTTP/1.1 < Host: api.openaq.org >HTTP/1.1 200 OK > Content-Type: application/json; charset=utf-8 > Transfer-Encoding: chunked > Connection: keep-alive > < "meta":< "name":"openaq-api", "license":"CC BY 4.0", "website":"https://docs.openaq.org/", "page":1, "limit":100, "found":1 >, "results":[ < "country":"BA", "name":"Goražde", "city":"Goražde", "count":70797, "locations":1 >] >

Тестирование и подражание запросам

Использование сторонних API создает болевую точку в разработке — их трудно тестировать. Инженеры из Sentry облегчили некоторые из этих проблем, написав библиотеку для имитации запросов во время разработки.

Вместо отправки HTTP-ответа на сервер getsentry/responses перехватывает HTTP-запрос и возвращает заранее заданный ответ, который вы добавили во время тестирования.

Лучше показать на примере.

import unittest import requests import responses class TestAPI(unittest.TestCase): @responses.activate # intercept HTTP calls within this method def test_simple(self): response_data = < "id": "ch_1GH8so2eZvKYlo2CSMeAfRqt", "object": "charge", "customer": , > # mock the Stripe API responses.add( responses.GET, "https://api.stripe.com/v1/charges", json=response_data, ) response = requests.get("https://api.stripe.com/v1/charges") self.assertEqual(response.json(), response_data)

Если выполняется HTTP-запрос, который не соответствует имитируемым ответам, возникает ошибка ConnectionError.

class TestAPI(unittest.TestCase): @responses.activate def test_simple(self): responses.add(responses.GET, "https://api.stripe.com/v1/charges") response = requests.get("https://invalid-request.com")
requests.exceptions.ConnectionError: Connection refused by Responses - the call doesn't match any registered mock. Request: - GET https://invalid-request.com/ Available matches: - GET https://api.stripe.com/v1/charges

Имитация поведения браузера

Если вы написали достаточно кода для веб-скрапера, вы заметите, что некоторые сайты возвращают разный HTML в зависимости от того, используете ли вы браузер или обращаетесь к сайту программно. Иногда это является мерой защиты от скраппинга, но обычно серверы используют User-Agent sniffing, чтобы выяснить, какой контент лучше всего подходит для конкретного устройства (например, настольного или мобильного)

Если вы хотите вернуть то же содержимое, которое отображает браузер, вы можете переопределить наборы заголовков User-Agent запросов с тем, что послал бы Firefox или Chrome.

import requests http = requests.Session() http.headers.update(< "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" >)

Источник

Оцените статью