Перевод статьи Python’s Requests Library (Guide) от замечательной команды Real Python, которая уже не в первый раз радует нас новыми интересными и полезными материалами о языке Python. И хотя в сети есть ее переводы, я немного адаптировал ее содержимое, добавив некоторые справочные данные. Так же я добавил подраздел об использовании механизма сессий при обращении к удаленным службам, которого как я считаю не хватало в оригинале статьи для понимания некоторых затрагиваемых вопросов.

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

В этой статье мы рассмотрим лишь некоторые из наиболее полезных возможностей, которые может предложить requests, а также основные способы настройки ее инструментов и оптимизации их работы для различных ситуаций, с которыми вы можете столкнуться в дальнейшем. Вы также ознакомитесь с тем, как эффективно использовать requests для предотвращения замедления работы (зависания) ваших приложений при взаимодействии с удаленными службами.

В этом руководстве вы узнаете, как:

  • отправлять запросы, используя самые распространенные HTTP методы;
  • настроить содержимое заголовков и отправляемых данных запросов, используя строку запроса и текст сообщения;
  • просмотреть содержимое ваших запросов и ответов;
  • отправлять на сервер аутентифицированные запросы;
  • корректно настроить параметры запросов таким образом, чтобы избежать резервного копирования данных или замедления работы вашего приложения.

Хотя я попытался включить в настоящее руководство столько информации, сколько нужно, чтобы понять все примеры кода, которые я включил в эту статью, но тем не менее я надеюсь, что для начала вы обладаете общими базовыми понятиями о работе протокола HTTP.

Начинаем работать с requests

И так начнем с установки библиотеки requests. Для этого выполните в терминале консоли следующую команду:

$ pip install requests

Если же вы предпочитаете использовать Pipenv для управления пакетами Python, то можете запустить на исполнение следующую команду:

$ pipenv install requests

Установка библиотеки requests в операционной системе Windows и Linux практически не отличаются. В сети достаточно материалов, в которых описан принцип работы с пакетами Python в обеих системах. Поэтому в этой статье касаться этих вопросов мы далее не будем.

После того, как мы установили requests, мы можем ее использовать в своем приложении. Импорт библиотеки requests в ваш код выглядит следующим образом:

import requests

Теперь, когда мы все подготовили, пришло время начинать наш путь по изучению основ работы с библиотекой requests. И наша первая цель научиться отправлять HTTP-запрос GET.

Запрос GET (GET request)

Такие методы запросов, как GET и POST, определяют, действие, которое вы пытаетесь выполнить при выполнении HTTP-запроса. Помимо GET и POST, есть еще несколько других достаточно распространенных методов запросов и их мы рассмотрим в этом руководстве позже.

И так одним из самых распространенных методов HTTP-запросов является GET. Метод GET предписывает, что вы пытаетесь получить или извлечь некоторые данные из указанного ресурса. Чтобы отправить GET запрос, необходимо вызвать метод requests.get(url).

Проверим это на практике: отправим GET-запрос к GitHub Root REST API, вызвав метод get() со следующим значением параметра url:

>>> requests.get('https://api.github.com')

Отлично! Мы отправили свой первый запрос. Давайте изучим содержимое полученного ответа.

Ответ (response)

Объект Response является мощным средством для просмотра содержимого и обработки результатов наших запросов. Давайте пошлем, рассмотренный нами выше, запрос GET еще раз, но в этот раз сохраним принятое значение с объектом ответа в переменной, и затем поближе познакомиться с его атрибутами (свойствами), а также поведением:

response = requests.get('https://api.github.com')

В этом примере кода, мы используя возможности библиотеки requests, перехватили содержимое ответа удаленного сервера, возвращаемое методом get(). И далее его значение, которое находится в экземпляре объекта Response, сохраним в переменную с именем response. Теперь мы можем использовать response для того, чтобы получить различную информацию о результатах нашего GET запроса .

Код состояния запроса (status codes)

Первым битом информации, которую вы можете получить от объекта ответа Response, является код состояния запроса к серверу status codes. Код состояния информирует вас о статусе нашего запроса.

Например, статус 200 OK означает, что ваш запрос был успешным, а статус 404 NOT FOUND означает, что искомый ресурс не найден. Существует много других кодов состояния запроса, которые могут дать вам более детальное представление о том, что же все таки произошло с отправленным запросом.

Используя свойство объекта response.status_code, мы можем получить доступ к коду состояния ответа, который вернул удаленный сервер:

>>> response.status_code
200

При обращении к свойству .status_code мы получили значение 200, что означает, что наш запрос был успешным, и сервер отправил нам данные, которые мы запрашивали.

В большинстве случаев эту информацию мы будем использовать для реализации в своем коде различной логики: управлять дальнейшей работой нашего приложения:

if response.status_code == 200:
    print('Success!')
elif response.status_code == 404:
    print('Not Found.')

В соответствии с логикой этого примера кода, если сервер возвращает код состояния 200, то наша программа напечатает «Success!». Если же — 404, то напечатает “Not Found”.

Библиотека requests существенно упрощает процесс взаимодействия вашего приложения с сервером. Однако необходимо знать ее некоторые довольно специфические особенности. Так например, если мы используем экземпляр объекта Response в условном выражении, то его логическое значение приравнивается к True, если был получен код состояния запроса в диапазоне от 200 до 400, и только в противном случае False.

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

if response:
    print('Success!')
else:
    print('An error has occurred.')

Маленькая техническая деталь: этот тест на истинность значения показал такой результат возможным, так как в объекте Response специальный метод класса __bool__() переопределен.

Это означает то, что поведение по умолчанию объекта Response при вычислении его логического значения было переопределено для процедуры проверки кода состояния запроса.

Имейте в виду, что этот способ проверки не гарантирует, что код состояния вашего запроса успешен и равен 200. Причиной прохождения проверки на истинность является то, что запрос может получать и другие “успешные” коды состояния в диапазоне от 200 до 400. Такие, например, как 204 NO CONTENT и 304 NOT MODIFIED, которые также можно считать в определенном смысле “успешными”, так как они определяют некоторый успешно обработанный сервером ответ на запрос. Например, код 204 информирует нас, что запрос был успешным, но тело сообщения ответа сервера не ничего не содержит.

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

Допустим, вы не хотите проверять код состояния ответа в операторе if. Вместо этого вы можете генерировать исключение специального типа HTTPError, если запрос был неудачен. Сделать это можно используя метод .raise_for_status() следующим образом:

import requests
from requests.exceptions import HTTPError

for url in ['https://api.github.com', 'https://api.github.com/invalid']:
    try:
        response = requests.get(url)

        # Если запрос был успешен, то исключение Exception не возбуждается
        response.raise_for_status()
    except HTTPError as http_err:
        print(f'HTTP error occurred: {http_err}')  # Python 3.6
    except Exception as err:
        print(f'Other error occurred: {err}')  # Python 3.6
    else:
        print('Success!')

Таким образом, если вы вызываете метод .raise_for_status(), то исключение типа HTTPError будет вызываться для определенных кодов состояния (не успешных запросов к серверу). И если код состояния определяет запрос как успешный, то приложение продолжит работу без возбуждения этого исключения.

Для дальнейшего чтения: если вам не знакомы f-строки Python 3.6, то я призываю вас срочно познакомится с ими, поскольку они являются отличным способом упростить использование строковых шаблонов.

И так мы изучили некоторые приемы работы с кодами состояния ответов сервера. Однако, когда мы посылаем GET запрос, нам редко нужна информация лишь о коде состояния. Обычно мы хотим получить нечто больше. Далее мы научимся получать и обрабатывать содержимое данных, которые сервер отправляет обратно в теле ответа.

Содержимое ответа (Content)

Ответ на успешный запрос GET часто содержит в сообщении некоторую ценную информацию, известную так же, как полезное содержимое (payload). Используя атрибуты и методы объекта Response, мы можем просматривать его содержимое в различных форматах.

Чтобы получить содержимое ответа в бинарном виде, мы можем использовать свойство Response.content:

>>> response = requests.get('https://api.github.com')
>>> response.content
b'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}","commit_search_url":"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}","emails_url":"https://api.github.com/user/emails","emojis_url":"https://api.github.com/emojis","events_url":"https://api.github.com/events","feeds_url":"https://api.github.com/feeds","followers_url":"https://api.github.com/user/followers","following_url":"https://api.github.com/user/following{/target}","gists_url":"https://api.github.com/gists{/gist_id}","hub_url":"https://api.github.com/hub","issue_search_url":"https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}","issues_url":"https://api.github.com/issues","keys_url":"https://api.github.com/user/keys","notifications_url":"https://api.github.com/notifications","organization_repositories_url":"https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}","organization_url":"https://api.github.com/orgs/{org}","public_gists_url":"https://api.github.com/gists/public","rate_limit_url":"https://api.github.com/rate_limit","repository_url":"https://api.github.com/repos/{owner}/{repo}","repository_search_url":"https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}","current_user_repositories_url":"https://api.github.com/user/repos{?type,page,per_page,sort}","starred_url":"https://api.github.com/user/starred{/owner}{/repo}","starred_gists_url":"https://api.github.com/gists/starred","team_url":"https://api.github.com/teams","user_url":"https://api.github.com/users/{user}","user_organizations_url":"https://api.github.com/user/orgs","user_repositories_url":"https://api.github.com/users/{user}/repos{?type,page,per_page,sort}","user_search_url":"https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"}'

Несмотря на то, что свойство Response.content предоставляет нам доступ к “сырым” байтам полезного содержимого ответа и в большинстве случаев мы будем преобразовывать их в строку с заданной кодировкой символов, например, UTF-8. Объект Response легко сделает это для нас, предоставляя доступ к свойству Response.text:

>>> response.text
'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}","commit_search_url":"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}","emails_url":"https://api.github.com/user/emails","emojis_url":"https://api.github.com/emojis","events_url":"https://api.github.com/events","feeds_url":"https://api.github.com/feeds","followers_url":"https://api.github.com/user/followers","following_url":"https://api.github.com/user/following{/target}","gists_url":"https://api.github.com/gists{/gist_id}","hub_url":"https://api.github.com/hub","issue_search_url":"https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}","issues_url":"https://api.github.com/issues","keys_url":"https://api.github.com/user/keys","notifications_url":"https://api.github.com/notifications","organization_repositories_url":"https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}","organization_url":"https://api.github.com/orgs/{org}","public_gists_url":"https://api.github.com/gists/public","rate_limit_url":"https://api.github.com/rate_limit","repository_url":"https://api.github.com/repos/{owner}/{repo}","repository_search_url":"https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}","current_user_repositories_url":"https://api.github.com/user/repos{?type,page,per_page,sort}","starred_url":"https://api.github.com/user/starred{/owner}{/repo}","starred_gists_url":"https://api.github.com/gists/starred","team_url":"https://api.github.com/teams","user_url":"https://api.github.com/users/{user}","user_organizations_url":"https://api.github.com/user/orgs","user_repositories_url":"https://api.github.com/users/{user}/repos{?type,page,per_page,sort}","user_search_url":"https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"}'

Поскольку для декодирования байтов bytes в строку str требуется схема кодирования, то requests в начале попытается угадать кодировку содержимого ответа на основе его заголовков Content-Type, в том если вы предварительно их не укажете. Вы также можете указать кодировку явно, установив значение свойства объекта Response.encoding перед обращением к Response.text:

>>> response.text
'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}","commit_search_url":"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}","emails_url":"https://api.github.com/user/emails","emojis_url":"https://api.github.com/emojis","events_url":"https://api.github.com/events","feeds_url":"https://api.github.com/feeds","followers_url":"https://api.github.com/user/followers","following_url":"https://api.github.com/user/following{/target}","gists_url":"https://api.github.com/gists{/gist_id}","hub_url":"https://api.github.com/hub","issue_search_url":"https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}","issues_url":"https://api.github.com/issues","keys_url":"https://api.github.com/user/keys","notifications_url":"https://api.github.com/notifications","organization_repositories_url":"https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}","organization_url":"https://api.github.com/orgs/{org}","public_gists_url":"https://api.github.com/gists/public","rate_limit_url":"https://api.github.com/rate_limit","repository_url":"https://api.github.com/repos/{owner}/{repo}","repository_search_url":"https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}","current_user_repositories_url":"https://api.github.com/user/repos{?type,page,per_page,sort}","starred_url":"https://api.github.com/user/starred{/owner}{/repo}","starred_gists_url":"https://api.github.com/gists/starred","team_url":"https://api.github.com/teams","user_url":"https://api.github.com/users/{user}","user_organizations_url":"https://api.github.com/user/orgs","user_repositories_url":"https://api.github.com/users/{user}/repos{?type,page,per_page,sort}","user_search_url":"https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"}'

Если вы обратите внимание на полученное содержимое ответа, то увидите, что оно представляет собой сериализованную строку в формате JSON. Поэтому для того, чтобы в результате получить словарь, вы можете взять строку str, полученную из свойства Response.text, и десериализовать ее с помощью метода json.loads(). Однако более простой способ выполнить эту задачу — использовать метод нашего объекта Response.json():

>>> response.json()
{'current_user_url': 'https://api.github.com/user', 'current_user_authorizations_html_url': 'https://github.com/settings/connections/applications{/client_id}', 'authorizations_url': 'https://api.github.com/authorizations', 'code_search_url': 'https://api.github.com/search/code?q={query}{&page,per_page,sort,order}', 'commit_search_url': 'https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}', 'emails_url': 'https://api.github.com/user/emails', 'emojis_url': 'https://api.github.com/emojis', 'events_url': 'https://api.github.com/events', 'feeds_url': 'https://api.github.com/feeds', 'followers_url': 'https://api.github.com/user/followers', 'following_url': 'https://api.github.com/user/following{/target}', 'gists_url': 'https://api.github.com/gists{/gist_id}', 'hub_url': 'https://api.github.com/hub', 'issue_search_url': 'https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}', 'issues_url': 'https://api.github.com/issues', 'keys_url': 'https://api.github.com/user/keys', 'notifications_url': 'https://api.github.com/notifications', 'organization_repositories_url': 'https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}', 'organization_url': 'https://api.github.com/orgs/{org}', 'public_gists_url': 'https://api.github.com/gists/public', 'rate_limit_url': 'https://api.github.com/rate_limit', 'repository_url': 'https://api.github.com/repos/{owner}/{repo}', 'repository_search_url': 'https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}', 'current_user_repositories_url': 'https://api.github.com/user/repos{?type,page,per_page,sort}', 'starred_url': 'https://api.github.com/user/starred{/owner}{/repo}', 'starred_gists_url': 'https://api.github.com/gists/starred', 'team_url': 'https://api.github.com/teams', 'user_url': 'https://api.github.com/users/{user}', 'user_organizations_url': 'https://api.github.com/user/orgs', 'user_repositories_url': 'https://api.github.com/users/{user}/repos{?type,page,per_page,sort}', 'user_search_url': 'https://api.github.com/search/users?q={query}{&page,per_page,sort,order}'}

Отлично, тип возвращаемого методом Response.json() значения словарь, поэтому мы можем, как обычно, получить доступ к его значениям по соответствующему ключу.

Вы можете по разному использовать информацию о коде состояния и содержимое полученного ответа сервера, но если нам нужна дополнительная информация, такая, например, как метаданные о самом ответе, то вам нужно иметь дело с заголовками полученного ответа.

Заголовки ответа (Headers)

Заголовки ответа сервера могут дать много полезной информации, такой, например, как тип полезного содержимого ответа, ограничение по времени, в течение которого ответ будет кэшироваться и т.д. Чтобы просмотреть содержимое заголовков, необходимо обратиться к свойству объекта Response.headers:

>>> response.headers
{'Server': 'GitHub.com', 'Date': 'Mon, 10 Dec 2018 17:49:54 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Status': '200 OK', 'X-RateLimit-Limit': '60', 'X-RateLimit-Remaining': '59', 'X-RateLimit-Reset': '1544467794', 'Cache-Control': 'public, max-age=60, s-maxage=60', 'Vary': 'Accept', 'ETag': 'W/"7dc470913f1fe9bb6c7355b50a0737bc"', 'X-GitHub-Media-Type': 'github.v3; format=json', 'Access-Control-Expose-Headers': 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type', 'Access-Control-Allow-Origin': '*', 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains; preload', 'X-Frame-Options': 'deny', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'origin-when-cross-origin, strict-origin-when-cross-origin', 'Content-Security-Policy': "default-src 'none'", 'Content-Encoding': 'gzip', 'X-GitHub-Request-Id': 'E439:4581:CF2351:1CA3E06:5C0EA741'}

При обращении к свойству Response.headers будет возвращен схожий со словарем объект, позволяющий получить доступ к значениям заголовков полученного ответа по ключу. Например, чтобы определить тип полезного содержимого ответа, получаем доступ к значению заголовка Content-Type:

>>> response.headers['Content-Type']
'application/json; charset=utf-8'

У этого, как мы уже говорили, схожим со словарем объекте заголовков есть еще одна особенность. Спецификация HTTP определяет названия заголовков без учета регистра, это означает, что мы можем получить доступ к их значениям, совершенно не беспокоясь об регистре их наименований:

>>> response.headers['content-type']
'application/json; charset=utf-8'

Используете ли вы ключ 'content-type' или 'Content-type', вы получите одно и то же корректное значение.

Теперь когда мы ознакомились с основами работы с объектом Response , рассмотрели его наиболее полезные свойства и методы в действии. Давайте сделаем шаг назад и посмотрим, как изменяются ответы сервера при использовании параметров запроса GET , передаваемых в строке запроса.

Параметры строки запроса (Query String Parameters)

Одним из самых распространенных способов настройки запроса GET является передача серверу данных в URL строки запроса.

Обратите внимание, что GET запрос не имеет тела сообщения. Но, это не означает, что с его помощью мы не можем передать серверу никакую информацию. Это можно делать с помощью специальных GET параметров. Чтобы добавить GET параметры к запросу, нужно в конце URL-адреса поставить знак ? и после него начинать задавать их по следующему правилу: имя_параметра1=значение_параметра1&имя_параметра2=значение_параметра2. Разделителем между параметрами служит знак &.

Для передачи GET параметров запроса необходимо передать нужную информацию в именованный параметр params метода get(). Например, следующим способом вы можете использовать Search API GitHub для более узкого поиска репозитория библиотеки requests:

import requests

# Поиск requests в репозиториях GitHub
response = requests.get(
    'https://api.github.com/search/repositories',
    params={'q': 'requests+language:python'},
)

# Просматриваем значения атрибутов результатов поиска репозитория requests
json_response = response.json()
repository = json_response['items'][0]
print(f'Repository name: {repository["name"]}')  # Python 3.6+
print(f'Repository description: {repository["description"]}')  # Python 3.6+

Передав словарь {'q': 'requests+language: python'} в качестве значения параметра params метода .get(), мы таким образом скорректировали результаты, возвращаемые Search API.

Мы можем передавать значения в params метода get() как в виде словаря, как мы это только что сделали, так и в виде списка кортежей:

>>> requests.get(
...     'https://api.github.com/search/repositories',
...     params=[('q', 'requests+language:python')],
... )

Так же можно передать данные в бинарном виде bytes:

>>> requests.get(
...     'https://api.github.com/search/repositories',
...     params=b'q=requests+language:python',
... )

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

Заголовки запроса (Request Headers)

Для того, чтобы используя requests, настроить содержимое заголовков запроса, необходимо передать словарь с соответствующими ключами и значениями HTTP-заголовков в метод .get(), а точнее в его именованный параметр headers. Например, изменим свой предыдущий поисковый запрос: добавим ключевые слова для уточнения результатов поиска, указав точно text-match текстовое соответствие для поиска (тип контента) в заголовок Accept:

import requests

response = requests.get(
    'https://api.github.com/search/repositories',
    params={'q': 'requests+language:python'},
    headers={'Accept': 'application/vnd.github.v3.text-match+json'},
)

# выведем в консоли массив всех совпадений `text-matches`,
# которые мы задали для уточнения результатов поиска
json_response = response.json()
repository = json_response['items'][0]
print(f'Text matches: {repository["text_matches"]}')

Заголовок Accept сообщает серверу, какие типы контента может обрабатывать ваше приложение. В нашем случае, поскольку вы хотите, что бы учитывались дополнительные уточняющие параметры поиска, вы устанавливаете для этого заголовка значение application / vnd.github.v3.text-match + json. Это значение является проприетарным для заголовка Accept, то есть зарезервированным GitHub для подобных случаев, и указывает на поиск содержимого в формате JSON.

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

Другие HTTP методы запроса

Помимо GET существуют и другие часто используемые HTTP методы, например, POST, PUT, DELETE, HEAD, PATCH и OPTIONS. И библиотека requests ожидаемо предоставляет методы, со схожей как у метода get() нотацией использования, для отправки и управления настройками каждого из этих HTTP методов запросов:

>>> requests.post('https://httpbin.org/post', data={'key':'value'})
>>> requests.put('https://httpbin.org/put', data={'key':'value'})
>>> requests.delete('https://httpbin.org/delete')
>>> requests.head('https://httpbin.org/get')
>>> requests.patch('https://httpbin.org/patch', data={'key':'value'})
>>> requests.options('https://httpbin.org/get')

В примере ниже каждый вызов соответствующего метода посылает запрос сервису httpbin, используя различные HTTP методы. И мы так же, как и ранее можем просмотреть содержимое их ответов:

>>> response = requests.head('https://httpbin.org/get')
>>> response.headers['Content-Type']
'application/json'

>>> response = requests.delete('https://httpbin.org/delete')
>>> json_response = response.json()
>>> json_response['args']
{}

Содержимое заголовков, полезного содержимого ответов, коды состояния и многие другие данные возвращаются с объектом Response для каждого вашего запроса, отправленного любым из методов.

Тело сообщения запроса

В соответствии со спецификацией HTTP протоколов, такие методы отправки запросов как POST, PUT, а так же другие менее распространенные, например PATCH, передают информацию в содержимом сообщения, а не через параметры строки запроса. Используя requests, вы можете передать полезную информацию в сообщении запроса через именованный параметр data соответствующего метода отправки.

Таким образом все отправляемые данные содержатся в самом теле вашего запроса. Кроме того, методом POST на удаленный сервер нередко загружаются файлы.

Параметр data принимает словарь, список кортежей, байтов или файлоподобный объект. В каком конкретно виде посылать данные в теле запроса вы должны выбрать самостоятельно, исходя из требований удаленной службы (сервера) с которой хотите взаимодействовать.

Например, если тип содержимого вашего запроса application / x-www-form-urlencoded, то вы можете отправить данные html формы в виде словаря:

>>> requests.post('https://httpbin.org/post', data={'key':'value'})

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

>>> requests.post('https://httpbin.org/post', data=[('key', 'value')])

В теле сообщения значения кодируются в виде кортежа с ключами, разделенными символом '&', и '=' между ключом и значением.

Если вам необходимо отправить данные в формате JSON, то вы должны использовать для передачи соответствующий именованный параметр json. При отправке запроса с содержимым JSON библиотека requests сама сериализует ваши данные и добавит соответствующее значение для заголовка Content-Type.

httpbin.org — отличный вспомогательный ресурс, созданный автором requests Кеннетом Рейтцем. Это сервис принимает тестовые запросы и отправляет в ответ информацию о них. Например, вы можете использовать его для проверки корректности вашего POST-запроса:

>>> response = requests.post('https://httpbin.org/post', json={'key':'value'})
>>> json_response = response.json()
>>> json_response['data']
'{"key": "value"}'
>>> json_response['headers']['Content-Type']
'application/json'

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

Инспектируем отправленный запрос

Когда вы делаете запрос, библиотека requests предварительно готовит запрос, прежде чем отправить его на целевой сервер. Подготовка запроса включает в себя такие вещи, как проверка корректности заголовков и сериализация содержимого в формате JSON.

Вы можете просмотреть содержимое подготовленного к отправке запроса, используя объект типа PreparedRequest, обратившись к свойству объекта Response.request:

>>> response = requests.post('https://httpbin.org/post', json={'key':'value'})
>>> response.request.headers['Content-Type']
'application/json'
>>> response.request.url
'https://httpbin.org/post'
>>> response.request.body
b'{"key": "value"}'

Просмотр содержимого PreparedRequest дает вам доступ ко всей информации о выполненном запросе, такой как полезное содержимое, URL, отправленные заголовки, данные аутентификации и многое другое.

В соответствии с официальной документацией объект типа PreparedRequest может использоваться и до отправки запроса. Так в следующим примере кода вначале с помощью конструктора Request(method, url, data=data, headers=headers) создается объект запроса, а затем с использованием метода Request.prepare() создается подготовленный запрос. Объект PreparedRequest может в дальнейшем, перед своей непосредственно отправкой, редактироваться в соответствии с логикой вашего приложения, а только затем отправлен с вызовом метода .send().

from requests import Request, Session

s = Session()

req = Request('POST', url, data=data, headers=headers)
prepped = req.prepare()

prepped.body = 'No, I want exactly this as the body.'

del prepped.headers['Content-Type']

resp = s.send(prepped,
    stream=stream,
    verify=verify,
    proxies=proxies,
    cert=cert,
    timeout=timeout
)

print(resp.status_code)

В этом примере кода мы можем заметить не знакомую нам строку кода: s = Session(). Дело в том, что в этом примере подготовленный запрос использовался для работы с сессионными cookie. В оригинале статьи автор почему-то опустил эту немаловажную возможность библиотеки requests: использование механизма сессий для работы с удаленными службами. Поэтому в следующем разделе мы с ним просто обязаны познакомиться ;).

Использование механизма сессий (Session)

Для организации сессионного взаимодействия вашего приложения с сервером используется объект Session, который позволяет сохранять необходимые настройки для отправки данных от запроса к запросу в течение всей текущей сессии. То есть он сохраняет и задает содержимое файлов cookie во всех запросах, сформированных с использованием текущего экземпляра объекта Session, и гарантирует использование выделенного пула соединений при дальнейшем взаимодействии с сервером. Объект Sesson поддерживает любые методы запросов из состава API requests.

И так давайте сохраним некоторые данные cookie в запросе:

import requests

s = requests.Session()

s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get('https://httpbin.org/cookies')

print(r.text)
# '{"cookies": {"sessioncookie": "123456789"}}'

В примере кода выше мы обратились к API httpbin.org для установки и поддержания соединения в течение сессии с указанным значением sessioncookie.

Механизм сессий также можно применять для предварительной установки значений некоторых настроек по умолчанию и дальнейшем их использовании при отправке запросов. Это делается путем изменения соответствующих свойств объекта Session:

import requests

s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})

# both 'x-test' and 'x-test2' are sent
s.get('https://httpbin.org/headers', headers={'x-test2': 'true'})

Все данные, которые вы затем будете передавать методу запроса, будут объединены с установленными значениями текущего сеанса сессии. При этом те параметры, которые вы будете передавать в метод запроса переопределят параметры сеанса сессии, которые мы задали по умолчанию.

Обратите внимание, что параметры, которые мы передаем в метод перед отправкой, не будут сохраняться между отдельными запросами, даже при работе в текущем сеансе сессии, даже если мы используем один и тот же тип метода запроса (в примере ниже это GET запрос). Как видно из результатов выполнения кода из примера ниже: мы отправляем данные cookie, передавая их методу отправки, только с первым запросом GET, но не со вторым:

import requests

s = requests.Session()

r = s.get('https://httpbin.org/cookies', cookies={'from-my': 'browser'})
print(r.text)
# '{"cookies": {"from-my": "browser"}}'

r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {}}'

Если вы хотите непосредственно управлять файлами cookie в течение текущего сеанса, используйте методы утилиты объекта Cookie, для установки соответствующих свойств объекта Session.cookies. Следующий пример кода иллюстрирует способ посылки двух запросов в течение сессии с заданными значениями файлов cookies:

import requests

s = requests.Session()
s.cookies.set('from-my', 'browser')

r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {"from-my": "browser"}}'

r = s.get('https://httpbin.org/cookies')
print(r.text)
# '{"cookies": {"from-my": "browser"}}'

И так мы рассмотрели несколько разновидностей отправляемых запросов, однако у них есть одно ограничение: они не позволяют отправлять аутентифицированные запросы к публичным API. Многие удаленные службы, с которыми вы можете столкнуться, требуют, чтобы вы каким-либо образом предоставляли аутентификационную информацию о себе.

Аутентификация

Как вам уже, вероятно, известно аутентификация по сути помогает удаленному сервису понять, кто вы. Как правило, вы предоставляете серверу свои учетные данные, передавая их через содержимое заголовка авторизации Authorization или любого другого пользовательского заголовка, заранее определенного удаленной службой. Все методы отправки запросов, с которыми мы ознакомились ранее, предоставляют пользователю возможность передавать значение именованному параметру auth, который позволяет передавать учетные данные для авторизации.

Одним из примеров такого API, взаимодействие с которым требуется аутентификация, является GitHub Authenticated User API. Эта точка доступа сервиса GitHub предоставляет информацию о профиле зарегистрированных пользователей. Чтобы сделать запрос к API авторизованного пользователя, вы можете передать свои имя пользователя GitHub и пароль в метод get(), предварительно упаковав их в кортеж:

>>> from getpass import getpass
>>> requests.get('https://api.github.com/user', auth=('username', getpass()))

Запрос будет успешно обработан сервером, при условии что учетные данные, которые вы передали в кортеже параметру auth, действительны. Если же вы попытаетесь передать этот запрос без учетных данных, то увидите, что результатом его выполнения будет получение ответа с кодом состояния 401 Unauthorized:

>>> requests.get('https://api.github.com/user')

Когда вы передаете параметру auth свои данные (имя пользователя и пароль) , при отправке запроса библиотека requests учитывает эти учетные данные, используя основную схему аутентификации доступа Basic access authentication scheme HTTP протокола.

Следующим способом с помощью объекта HTTPBasicAuth, вы можете послать тот же запрос, но уже явно передав свои учетные данные, используя механизм Basic authentication:

>>> from requests.auth import HTTPBasicAuth
>>> from getpass import getpass
>>> requests.get(
...     'https://api.github.com/user',
...     auth=HTTPBasicAuth('username', getpass())
... )

В общем случае можно конечно явно не указывать Basic authentication в качестве основного способа аутентификации при отправке вашего запроса, вы можете реализовать эту процедуру любым другим способом. requests так же предоставляет другие методы аутентификации “из коробки”, например, дайджест-аутентификация HTTPDigestAuth или прокси-аутентификация HTTPProxyAuth.

Вы даже можете разработать свой собственный механизм аутентификации с удаленной службой и затем использовать его в ваших приложениях. Для этого вы должны сначала создать подкласс от основного AuthBase, а затем реализовать в объявлении своего класса собственный метод __call __ ():

import requests

from requests.auth import AuthBase

class TokenAuth(AuthBase):
    """Реализуем собственный способ аутентификации"""

    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        """Подключаем к токену API свой заголовок аутентификации"""
        r.headers['X-TokenAuth'] = f'{self.token}'  # Python 3.6+
        return r

requests.get('https://httpbin.org/get', auth=TokenAuth('12345abcde-token'))

Здесь ваш настраиваемый механизм TokenAuth получает токен, а затем включает этот токен в заголовок X-TokenAuth вашего запроса.

При реализации собственных способов аутентификации помните, что простые механизмы проверки подлинности могут привести к уязвимостям безопасности, поэтому, если удаленной службе по какой-либо причине не требуется настраиваемый механизм проверки подлинности, всегда используйте проверенные схемы аутентификации, такие как Basic authentication или OAuth.

И так пока мы задумались о безопасности, давайте рассмотрим вопросы применения в ваших запросах SSL-сертификатов.

Проверка подлинности SSL сертификата (SSL Certificate Verification)

Всякий раз, когда данные, которые вы пытаетесь отправить или получить, являются конфиденциальными, вы начинаете думать о вопросах их безопасности. Вы общаетесь с защищенными сайтами используя протокол HTTP, устанавливая шифрованное соединение с использованием SSL, это означает, что проверка SSL-сертификата целевого сервера имеет решающее значение.

Хорошей новостью является то, что используя в ваших проектах библиотеку requests, она делает это для вас по умолчанию. Тем не менее в некоторых случаях вы можете захотеть изменить или немножко подкорректировать это поведение.

И так если вы захотите отключить проверку SSL-сертификата на удаленном сервере, достаточно передать значение False в именованный параметр verify:

>>> requests.get('https://api.github.com', verify=False)
InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning

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

Примечание: requests использует другой отдельный пакет certifi для предоставления сертификатов. Это позволяет библиотеке requests знать, каким организациям, выпускающим сертификаты, можно доверять. Поэтому вам следует как можно чаще обновлять пакет certifi, установленный в вашей системе, чтобы обеспечить максимальную безопасность ваших соединений.

Производительность

При использовании requests, особенно при работе ваших приложений непосредственно в production environment среде, важно учитывать влияние производительности. Такие возможности приложения, как контроль над времени ожидания ответа сервера, сессиями и ограничение повторных попыток обращения к удаленным службам, помогут обеспечить бесперебойную и устойчивую работу вашего приложения.

Тайм-ауты Timeouts

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

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

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

По умолчанию requests будет ждать ответа на запрос от сервера до бесконечности, поэтому рекомендуется практически во всех случаях указывать интервал ожидания, чтобы избежать всех, перечисленных выше, негативных последствий. Чтобы установить интервал времени ожидания запроса, используйте именованный параметр timeout. Значение timeout может быть целым числом или числом с плавающей запятой, задающее количество секунд ожидания ответа от сервера до истечения времени ожидания:

>>> requests.get('https://api.github.com', timeout=1)

>>> requests.get('https://api.github.com', timeout=3.05)

В первом запросе время ожидания истекает через 1 секунду. Во втором — через 3,05 секунды.

Рекомендуется устанавливать timeout чуть больше 3 секунд, что определяется величиной кратной длительности по умолчанию окна повторной передачи TCP-пакетов.

В качестве значения параметра timeout можно так же передать кортеж, состоящий из двух значений (connect, read). Его первый элемент connect определяет тайм-аут соединения (время, которое выделяется на установление соединения с удаленным сервером), а второй read — тайм-аут чтения (время, ожидания ответа от сервера, после того, как соединение было установлено):

>>> requests.get('https://api.github.com', timeout=(2, 5))

И так если наш запрос устанавливает соединение с сервером в течение 2 секунд и получает от него данные в течение 5 секунд после установления соединения, то содержимое ответа сервера будет возвращено, как это было и раньше. Если же время ожидания истекло, то будет генерироваться исключение типа Timeout:

import requests
from requests.exceptions import Timeout

try:
    response = requests.get('https://api.github.com', timeout=1)
except Timeout:
    print('The request timed out')
else:
    print('The request did not time out')

Таким образом, при необходимости код вашего приложения может перехватить исключение Timeout и обработать его соответствующим образом.

Объект сессии Session

До сих пор мы имели дело с сетевыми интерфейсами запросов и API высокого уровня, такими как методы get() и post(). Эти методы абстрагирует пользователя от того, что происходит “под капотом” библиотеки, когда мы отправляем запросы. Они скрывают детали реализации, такие как управление соединениями, так что нам не приходится об этом задумывать при написании своих приложений.

Одну из основных ролей при абстрагировании пользователя от деталей работы библиотеки, играет класс Session. Если вы хотите осуществлять непосредственный контроль над выполнением запросов, а так же повысить их производительность, то вам необходимо использовать все возможности экземпляров класса Session.

Как мы уже знаем, сессии используются для сохранения некоторого набора настроек от запроса к запросу при обращении к какое-либо удаленной службе. Например, если вы хотите использовать одни и те же данные аутентификации в течение определенного периода времени для отправки нескольких запросов, то можете использовать сессии следующим образом:

import requests
from getpass import getpass

# Используем менеджер контекста так как в случае завершении работы сессии
# все использующиеся ресурсы будут автоматически освобождены
with requests.Session() as session:
    session.auth = ('username', getpass())

    # вместо requests.get(), используем session.get()
    response = session.get('https://api.github.com/user')

# просматриваем содержимое ответа
print(response.headers)
print(response.json())

Каждый раз, когда вы делаете запрос в течение сессии, начиная с того момента, как он был инициализирован с вашими учетными данными аутентификации, эти данные будут сохраняться в течении всей сессии, то есть пока вы используете текущий экземпляр класса Session.

Первичная оптимизация производительности с использованием механизма сессий происходит по причине того, что устанавливается постоянное соединение с сервером. Когда ваше приложение устанавливает соединение с сервером, используя сессии, происходит сохранение текущего соединения в общем пуле сервера. И когда ваше приложение снова захочет подключиться к тому же серверу, то будет повторно использоваться соединение из пула, а не устанавливать новое. Это позволяет сократить время обращения за данными к удаленной службе, что конечно же отражается на производительности вашего приложения.

Количество повторов запроса

В случае если ваш запрос по той или иной причине был неудачен, то вы можете указать приложению повторить тот же запрос заданное число раз. Однако requests не будет это делать для вас по умолчанию. Чтобы использовать эту возможность, необходимо реализовать свой так называемый транспортный адаптер Transport Adapter.

Транспортные адаптеры позволяют вам определить набор конфигураций (настроек) для взаимодействия с конкретной удаленной службой. Допустим, вы хотите, чтобы все запросы к точке доступа https://api.github.com повторялись три раза, прежде чем, наконец, будет генерироваться исключение типа ConnectionError.

import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError

github_adapter = HTTPAdapter(max_retries=3)

session = requests.Session()

# Используйте `github_adapter` для всех запросов к конечным точкам, 
# которые начинаются с этого URL
session.mount('https://api.github.com', github_adapter)

try:
    session.get('https://api.github.com')
except ConnectionError as ce:
    print(ce)

В тех случаях, когда вы используете объект github_adapter класса HTTPAdapter в текущей сессии, в объекте session будут сохраняться заданные вами свойства конфигурации для каждого запроса, отправляемого по адресу https://api.github.com.

Вызов метода монтирования регистрирует новый экземпляр транспортного адаптера для указанного префикса (подстроки адреса). После его подключения любой HTTP-запрос, выполненный с использованием этой сессии, URL-адрес которого начинается с указанного префикса, будет использовать транспортный адаптер github_adapter. При создании нового объекта класса HTTPAdapter именованному параметру max_retries передается значение 3, количество повторов запроса, после чего будет генерироваться исключение ConnectionError. С другими настройками транспортных адаптеров, а так же, для каких целей могут использоваться, вы можете ознакомиться в официальной документации.

Заключение

И так мы прошли долгий путь в изучении возможностей Python библиотеки requests.

Теперь вы умеете:

  1. Отправлять запросы, используя различные методы протокола HTTP, такие как GET, POST и т.д.
  2. Управлять отправляемыми запросами с помощью настроек, изменять их заголовки, данные аутентификации, строку запроса и полезное содержимое сообщений.
  3. Просматривать и обрабатывать данные, которые вы отправляете на сервер, а так же данные, которые сервер посылает вам обратно.
  4. Использовать процедуру проверки SSL-сертификатов в ходе взаимодействия с удаленным сервером.
  5. Эффективно использовать различные возможности requests для повышения эффективности и производительности ваших приложений, например, max_retries, timeout, Sessions и Transport Adapters.

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

1 комментарий

Оставить комментарий