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

В этом руководстве рассматриваются некоторые дополнительные возможности библиотеки Requests.

Объект Session сеанса (сессии) браузера

Объект Session позволяет от запроса к запросу сохранять определенные (заданные пользователем) параметры сеанса (сессии) браузера. В экземпляре объекта Session сохраняются значения данных cookies , а также может использоваться urllib3 пул соединений. Поэтому, если вы делаете несколько запросов к одному хосту, то основные настройки TCP-соединения могут использоваться повторно, что существенно увеличит производительность вашего кода (см. Постоянное соединение HTTP).

Объект Session включает в себя все основные методы Requests API.

Давайте сохраним некоторые данные cookies текущего сеанса сеанса (сессии) для использования при дальнейшей отправке запросов:

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"}}'

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

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

# будут отправлены оба заголовка 'x-test' и 'x-test2'
s.get('https://httpbin.org/headers', headers={'x-test2': 'true'})

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

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

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": {}}'

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

Сессии также могут использоваться вместе с уже привычными нам менеджерами контекста:

with requests.Session() as s:
    s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')

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

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

Иногда возникает необходимость исключить какой-либо параметр из задаваемого вами набора настроек сеанса (сессии), который, как нам уже известно, представляется в виде словаря dict, по наименованию ключа. Для этого достаточно при формировании конкретного запроса установить значение соответствующего ключа равным None. И далее при отправке этого запроса нужный параметр из набора настроек будет автоматически опущен.

Все значения настроек, содержащиеся в объекте текущего сеанса (сессии), доступны напрямую. Чтобы больше узнать о том, как работать с сессиями в Requests ознакомьтесь с документацией Session API.

Объекты запроса и ответа

Всякий раз, когда вы формируете запрос с использованием метода questions.get() либо любых других методов, вы, по сути, проделываете две основные процедуры.

Во-первых, сначала вы создаете объект типа Request, содержимое которого затем будет отправлено на сервер, для получения его ответа, или какого-либо ресурса.

Во-вторых, как только вы с помощью Requests получаете ответ от сервера, генерируется объект Response. Объект Response содержит всю информацию возвращаемую сервером, а также сам объект запроса Request, сформированный вами изначально. Рассмотрим пример простого запроса для получения очень важной информации от сервера Википедии:

>>> r = requests.get('https://en.wikipedia.org/wiki/Monty_Python')

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

>>> r.headers
{'content-length': '56170', 'x-content-type-options': 'nosniff', 'x-cache':
'HIT from cp1006.eqiad.wmnet, MISS from cp1010.eqiad.wmnet', 'content-encoding':
'gzip', 'age': '3080', 'content-language': 'en', 'vary': 'Accept-Encoding,Cookie',
'server': 'Apache', 'last-modified': 'Wed, 13 Jun 2012 01:33:50 GMT',
'connection': 'close', 'cache-control': 'private, s-maxage=0, max-age=0,
must-revalidate', 'date': 'Thu, 14 Jun 2012 12:59:39 GMT', 'content-type':
'text/html; charset=UTF-8', 'x-cache-lookup': 'HIT from cp1006.eqiad.wmnet:3128,
MISS from cp1010.eqiad.wmnet:80'}

Если же мы хотим получить содержимого заголовков соответствующего запроса к серверу, то просто обращаемся к нашему объекту запроса, а точнее непосредственно к его заголовкам, используя свойство headers:

>>> r.request.headers
{'Accept-Encoding': 'identity, deflate, compress, gzip',
'Accept': '*/*', 'User-Agent': 'python-requests/1.2.0'}

Подготовленные запросы

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

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

from requests import Request, Session

s = Session()

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

# делаем что-то с prepped.body
prepped.body = 'No, I want exactly this as the body.'

# делаем что-то с prepped.headers
del prepped.headers['Content-Type']

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

print(resp.status_code)

Поскольку в начале вы непосредственно ничего не делаете с объектом Request, то сначала подготавливаете запрос, а затем, как вам угодно в любом месте вашего приложения, изменяете его полезное содержимое, либо настройки, используя для этого объект PreparedRequest. Позже вы просто отправляете его с уже измененными параметрами, так, как вы отправляли бы его используя методы requests.* или Session.*.

Однако надо помнить, что использование этого приема приведет к утрате некоторых преимуществ использования объекта Session. В частности, это коснётся передачи данных текущего состояния сеанса (сессии), например, содержимое данных cookies не будет использоваться при отправке запроса. Для того, чтобы отправить подготовленный запрос PreparedRequest с сохранением и использованием данных текущего состояния сеанса (сессии), необходимо заменить вызов метода Request.prepare() на Session.prepare_request(), следующим образом:

from requests import Request, Session

s = Session()
req = Request('GET',  url, data=data, headers=headers)

prepped = s.prepare_request(req)

# делаете что-либо с содержимым запроса prepped.body
prepped.body = 'Seriously, send exactly these bytes.'

# делаете что-либо с содержимым заголовков prepped.headers
prepped.headers['Keep-Dead'] = 'parrot'

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

print(resp.status_code)

Если вы используете подготовленные запросы в потоке flow, то имейте в виду, что при этом не будут учитываться текущие параметры environment среды исполнения скрипта. Это может создать проблемы, если вы используете переменные среды environment variables для изменения поведения или настроек запросов. Например: данные сертификатов SSL, содержащиеся в REQUESTS_CA_BUNDLE переменной среды, не будут приниматься во внимание при отправке запроса. Как результат проверки SSL будет возбуждено исключение типа: CERTIFICATE_VERIFY_FAILED. Вы можете обойти этот «подводный камень», явно объединив настройки среды выполнения с настройками текущего сеанса (сессии):

from requests import Request, Session

s = Session()
req = Request('GET', url)

prepped = s.prepare_request(req)

# объединим настройки окружения и сеанса (сессии)
settings = s.merge_environment_settings(prepped.url, {}, None, None, None)
resp = s.send(prepped, **settings)

print(resp.status_code)

Проверка SSL сертификата

Requests имеет возможность осуществлять проверку SSL сертификатов при выполнении HTTPS запросов, точно так же, как это делает обычный веб-браузер. По умолчанию проверка SSL включена, и поэтому Requests возбуждает исключение типа SSLError, если не может проверить его подлинность:

>>> requests.get('https://requestb.in')
requests.exceptions.SSLError: hostname 'requestb.in' doesn't match either of '*.herokuapp.com', 'herokuapp.com'

Из примера видим, что если у меня нет настроек SSL для этого домена, то будет возбуждается исключение. А вот для GitHub будет получен следующий результат:

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

В именованный аргумент verify вы можете передать строку, содержащую путь к файлу CA_BUNDLE (документ, содержащий корневой и промежуточный сертификаты соответствующего центра сертификации) или каталогу с достоверными сертификатами CA:

>>> requests.get('https://github.com', verify='/path/to/certfile')

Если в аргумент verify передан путь к каталогу, то каталог должен быть обработан с использованием утилиты c_rehash, поставляемой с OpenSSL.

Напомним, что по умолчанию для проверки SSL установлено значение True. Опция проверки сертификатов будет применяется только к сертификатам определенного хоста.

SSL сертификаты на стороне клиента

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

>>> requests.get('https://kennethreitz.org', cert=('/path/client.cert', '/path/client.key'))

или для постоянного использования:

s = requests.Session()
s.cert = '/path/client.cert'

Если вы укажете неправильный путь или неверный сертификат, то будет возбуждено исключение типа SSLError:

>>> requests.get('https://kennethreitz.org', cert='/wrong_path/client.pem')
SSLError: [Errno 336265225] _ssl.c:347: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib

Внимание!!! Закрытый ключ к вашему локальному сертификату не должен быть закодированым unencrypted. В настоящее время Requests не поддерживает использование кодированных (шифрованных) ключей.

CA сертификаты

Requests в своей работе использует сертификаты импортируемые из пакета certifi. Эта возможность позволяет пользователю обновлять свои доверенные сертификаты вне зависимости от установленной версии Requests. До версии 2.16 Requests собирал набор доверенных корневых сертификатов CA, из источника Mozilla trust store. Сертификаты обновлялись только один раз для каждой версии Requests. Если пакет certifi не будет установлен, то это приведет к необходимости использования устаревших наборов сертификатов с более старыми версиями Requests.

В целях безопасности мы рекомендуем вам как можно чаще обновлять ваши сертификаты!

Работа с потоковым содержимым запроса

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

tarball_url = 'https://github.com/psf/requests/tarball/master'
r = requests.get(tarball_url, stream=True)

После выполнения этого кода будут загружены только заголовки ответа, а соединение остается открытым, что позволяет получить содержимое контента только при выполнении определенного условия. Например:

if int(r.headers['content-length']) 
                

Используя методы Response.iter_content() и Response.iter_lines() вы можете управлять процессом обработки данных. Кроме того, недекодированное «сырое» содержимое ответа, полученное из объекта ответа Response.raw, вы можете прочитать с помощью метода urllib3 urllib3.HTTPResponse.

Если при формировании запроса в аргумент stream передается значение True, то Requests не сможет самостоятельно освободить текущее соединение (а соответственно оно не будет возвращено в общий пул соединений сервера) до тех пор пока все данные ответа не будут получены полностью или вами не будет вызван метод Response.close. Это может привести к неэффективному использованию пула соединений. И если вы обнаружите, что вам нужно прочитать только часть содержимого тела ответа (или вы вообще прекратить их чтение), то при использовании настройки stream = True, вы должны делать запрос в контексте оператора with, чтобы соединение всегда закрывалось автоматически после окончания работы вашего кода:

with requests.get('https://httpbin.org/get', stream=True) as r:
    # Здесь вы что-то делаете с содержимым ответа запроса.

Поддержка открытых Keep-Alive соединений

Отличные новости — благодаря возможностям urllib3 возможно поддержание активности соединения на 100% автоматически в течение всего времени текущего сеанса (сессии)! Любые запросы, которые вы делаете в течение сеанса (сессии), автоматически повторно используют соответствующее ранее уже открытое соединение!

Обратите внимание, что соединения освобождаются обратно в пул для повторного использования только после прочтения всех данных тела запроса. Поэтому в обязательном порядке для освобождения текущего соединения установите значение параметра stream равным False, либо прочитайте значение свойства content объекта Response.

Потоковая загрузка данных

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

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Внимание!!! Настоятельно рекомендуется открывать файлы для передачи в двоичном режиме. Это связано с тем, что Requests может попытаться автоматически заполнить для вас заголовок Content-Length, и если это произойдет, то это значение будет установлено равным количеству байтов в файле. В последствии могут возникнуть ошибки при открытия файлов в текстовом режиме.

Запросы с использованием механизма Chunked Transfer Encoding

Requests поддерживает механизм передачи данных Chunked Transfer Encoding для входящих и исходящих запросов.

Chunked Transfer Encoding — механизм передачи данных с помощью обычного протокола передачи гипертекста, позволяющий передавать данные от сервера клиенту без необходимости заранее указывать точный размер тела сообщения).

Для отправки данных в chunk-encoded запросе, используйте генератор (или вообще любой итератор без указания его длины), который затем буде передаваться в качестве содержимого запроса к серверу:

def gen():
    yield 'hi'
    yield 'there'

requests.post('http://some.url/chunked', data=gen())

Для обработки chunked encoded содержимого ответа сервера рекомендуется использовать метод Response.iter_content(). При формировании запроса вы также можете установить значение параметра stream = True. В этом случае вы сможете проитерировать полученное содержимое ответа, используя метод iter_content(), предварительно установив значение параметра chunk_size равным None. Если же вы хотите установить максимальный размер блока данных считываемого в память, то в качестве значения параметра chunk_size можете задать любое целое число, определяющее его размер в байтах.

Загрузка файлов методом POST с Content-Type:multipart/form-data

В одном запросе вы можете посылать серверу сразу несколько файлов. Например, предположим, что вы хотите загрузить несколько файлов изображений, используя интерфейс HTML формы с атрибутом name="images", с соответствующими значениями атрибутов type, multiple и полем name="images":

Для их отправки упакуйте данные о передаваемых файлах в список кортежей с соответствующими значениями (form_field_name, file_info):

>>> url = 'https://httpbin.org/post'
>>> multiple_files = [
        ('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
        ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
>>> r = requests.post(url, files=multiple_files)
>>> r.text
{
  ...
  'files': {'images': 'data:image/png;base64,iVBORw ....'}
  'Content-Type': 'multipart/form-data; boundary=3131623adb2043caaeb5538cc7aa0b3a',
  ...
}

Внимание!!! Настоятельно рекомендуется открывать файлы для передачи в двоичном режиме. Это связано с тем, что Requests может попытаться автоматически заполнить для вас заголовок Content-Length, и если это произойдет, то это значение будет установлено равным количеству байтов в файле. В последствии могут возникнуть ошибки при открытия файлов в текстовом режиме.

Хуки событий

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

Так, например, для использования доступны следующие хуки:

response: Requests сгенерировала ответ, полученный от сервера.

Вы можете назначить функцию обратного вызова (функцию обработчик) для определенного хука в каждом запросе текущего объекта запроса, передав при формировании запроса словарь вида {hook_name: callback_function} (название_хука: функция_обратного_вызова) в аргумент hooks:

hooks={'response': print_url}

При этом функция обратного вызова callback_function получит данные, полученного ответа в качестве своего первого аргумента.

def print_url(r, *args, **kwargs):
    print(r.url)

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

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

def record_hook(r, *args, **kwargs):
    r.hook_called = True
    return r

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

>>> requests.get('https://httpbin.org/', hooks={'response': print_url})
https://httpbin.org/

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

>>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]})
>>> r.hook_called
True

Также вы можете добавлять обработчики хуков к экземпляру объекта Session. При этом любые добавленные вами хуки будут срабатывать должным образом при каждом запросе в течение всего текущего сеанса (сессии). Например:

>>> s = requests.Session()
>>> s.hooks['response'].append(print_url)
>>> s.get('https://httpbin.org/')
 https://httpbin.org/
 

Объект Session также может содержать несколько обработчиков для отдельных хуков, которые в последствии будут вызываться в порядке их добавления.

Пользовательские способы аутентификации

Requests позволяет использовать свой собственный механизм аутентификации.

Любой вызываемый объект, передаваемый в качестве аргумента auth методу запроса, имеет возможность изменить содержимое данных запроса до его отправки.

Для реализации аутентификации используются подклассы, наследующие от базового класса AuthBase. Requests «из коробки» предоставляет две общие схемы аутентификации с использованием модуля request.auth: HTTPBasicAuth и HTTPDigestAuth.

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

from requests.auth import AuthBase

class PizzaAuth(AuthBase):
    """ Присоединяет HTTP Pizza аутентификацию к объекту запроса Request."""
    def __init__(self, username):
        # здесь задаем любые связанные с аутентификацией данные
        self.username = username

    def __call__(self, r):
        # модифицируем и возвращаем запрос
        r.headers['X-Pizza'] = self.username
        return r

Затем мы можем отправить запрос, используя наш новый способ аутентификации PizzaAuth:

>>> requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))

Потоковые запросы Streaming Requests

Используя метод Response.iter_lines() вы легко можете реализовать обработку данных, получаемых из потоковых API, в цикле (итерации), например, Twitter Streaming API. Для этого достаточно присвоить аргументу stream метода запроса значение True, а затем полученное содержимое ответа обрабатывать с использованием специального метода iter_lines() следующим образом:

import json
import requests

r = requests.get('https://httpbin.org/stream/20', stream=True)

for line in r.iter_lines():

    # здесь обрабатываем полученные данные
    if line:
        decoded_line = line.decode('utf-8')
        print(json.loads(decoded_line))

Передавая значение аргументу decode_unicode = True при вызове методов Response.iter_lines()или Response.iter_content(), мы можем предусмотреть установку резервной кодировки по умолчанию на случай, если сервер не предоставит ее тип в заголовках ответа явно:

r = requests.get('https://httpbin.org/stream/20', stream=True)

if r.encoding is None:
    r.encoding = 'utf-8'

for line in r.iter_lines(decode_unicode=True):
    if line:
        print(json.loads(line))

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

lines = r.iter_lines()

# Сохраним значение данных первой линии или просто пропустим их
first_line = next(lines)

for line in lines:
    print(line)

Использование прокси

Если вы хотите использовать в своих запросах прокси, то можете настроить каждый формируемый запрос индивидуально, используя для этого при вызове метода запроса аргумент proxies:

import requests

proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}

requests.get('http://example.org', proxies=proxies)

Также можно настроить адреса прокси, установив соответствующие значения переменным окружения HTTP_PROXY и HTTPS_PROXY.

$ export HTTP_PROXY="http://10.10.1.10:3128"
$ export HTTPS_PROXY="http://10.10.1.10:1080"

$ python
>>> import requests
>>> requests.get('http://example.org')

Для того, чтобы использовать с прокси свой собственный способ аутентификации, например, HTTP Basic Auth, то используйте синтаксис вида http: // user: password @ host /:

proxies = {'http': 'http://user:[email protected]:3128/'}

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

proxies = {'http://10.20.1.128': 'http://10.10.1.10:5323'}

Обратите внимание, URL прокси в обязательном порядке должен содержать схему обращения к ресурсу (сетевой протокол).

Сокеты

Новое в версии 2.10.0.

В дополнение к основным возможностям HTTP-прокси, Requests позволяет осуществлять обмен данных с прокси-серверами, использующими протокол SOCKS. Использование этой возможности требует установки дополнительных сторонних библиотек.

Вы можете самостоятельно установить все необходимые зависимости с помощью pip:

$ pip install requests[socks]

После установки зависимостей использовать SOCKS-прокси так же просто, как мы использовали прокси в HTTP:

proxies = {
    'http': 'socks5://user:pass@host:port',
    'https': 'socks5://user:pass@host:port'
}

При использовании схемы socks5 разрешение имен DNS происходит на клиенте, а не на прокси-сервере. Это аналогично использованию с curl соответствующей команды для того, чтобы решить делать ли DNS-разрешение имен на клиенте или прокси. Если вы хотите разрешать имена доменов на прокси-сервере, используйте в качестве схемы подключения socks5h.

Соответствие стандартам

Библиотека Requests обеспечивает соответствие всем актуальным спецификациям и RFC. Обычно это особенность библиотеки не вызовет затруднений у пользователей. Тем не менее такое внимание к соблюдению всех требований спецификаций может привести к поведению кода, которое может показаться необычным для тех пользователей библиотеки кто не знаком с их требованиями.

Кодировки

Когда вы получаете ответ от сервера, Requests делает предположение о типе его кодировки (на основе содержимого заголовков ответа) и в дальнейшем эта кодировка будет использоваться для декодирования содержимого ответа при обращении к атрибуту Response.text . При этом Requests сначала проверяет наличие сведений о кодировке в HTTP заголовках ответа, и если их там нет, то далее будет использоваться chardet логика для определения её типа.

Единственный случай, когда Requests не будет этого делать, если тип кодировки явно не задан в заголовках HTTP, а заголовок Content-Type содержит значение text. В этом случае RFC 2616 явно указывает, что тип кодировки содержимого соответствует ISO-8859-1. То есть Requests в этом случае будет строго следовать предписаниям спецификации. Если вам требуется другая кодировка, вы можете сначала вручную установить значение свойства Response.encoding, а затем уже использовать содержимое Response.content.

HTTP методы

Requests предоставляет для отправки запросов доступ почти ко всему диапазону HTTP методов: GET, OPTIONS, HEAD, POST, PUT, PATCH и DELETE и т.д. Ниже мы приведем примеры их использования при обращении к API GitHub.

Сначала мы начнем с наиболее часто используемого метода GET. HTTP GET — это идемпотентный метод, который возвращает ресурс затребованный по заданному URL. Как результат работы этого метода получение некоторых данных от веб ресурса. Рассмотрим следующий пример его использования: попытаемся получить информацию от GitHub о конкретном коммите. Предположим, что мы хотим зафиксировать все изменения в коммите a050faf исходного кода Requests. Это мы можем сделать следующим образом:

>>> import requests
>>> r = requests.get('https://api.github.com/repos/psf/requests/git/commits/a050faf084662f3a352dd1a941f2c7c9f886d4ad')

Определим корректно ли нам ответил GitHub. И если да, то с каким типом контента далее будем работать:

>>> if r.status_code == requests.codes.ok:
...     print(r.headers['content-type'])
...
application/json; charset=utf-8

GitHub возвращает ответ с содержимым в формате JSON. Это здорово, и теперь мы можем использовать метод r.json для его парсинга и передачи в объекты Python.

>>> commit_data = r.json()

>>> print(commit_data.keys())
[u'committer', u'author', u'url', u'tree', u'sha', u'parents', u'message']

>>> print(commit_data[u'committer'])
{u'date': u'2012-05-10T11:10:50-07:00', u'email': u'[email protected]', u'name': u'Kenneth Reitz'}

>>> print(commit_data[u'message'])
makin' history

Пока все просто. Чтож, теперь подробнее разберемся с API GitHub. Мы конечно могли бы предварительно ознакомится с его документацией, но нам было бы веселее, если бы мы для этого использовали возможности Requests. Воспользуемся методом Requests OPTIONS, чтобы определить какие виды HTTP методов поддерживаются при переходе по URL, который мы только что использовали.

>>> verbs = requests.options(r.url)
>>> verbs.status_code
500

Что это? Этот метод в нашем случае бесполезен! Оказывается GitHub, как и многие провайдеры API, на самом деле не реализует метод OPTIONS. Это досадный недосмотр, но все в порядке, теперь мы все же должны воспользоваться скучной документацией. Однако, если бы GitHub корректно реализовывал метод запроса OPTIONS, то он должен возвращать в заголовках список разрешенных методов.

>>> verbs = requests.options('http://a-good-website.com/api/cats')
>>> print(verbs.headers['allow'])
GET,HEAD,POST,OPTIONS

Обратившись к документации, мы увидим, что единственным методом, разрешенным для передачи коммитов, является метод POST, который создает новый коммит. Поскольку мы используем рабочий репозиторий Requests, то нам, вероятно, следует избегать делать к нему настоящие запросы POST. Вместо этого давайте поработаем с функцией GitHub Issues.

Допустим в документацию Requests в ответ на Issue #482 были добавлены некоторые изменения. Учитывая, что эта проблема еще актуальна, то здесь мы будем использовать ее в качестве реального примера. Давайте начнем с получения информации о Issue #482.

>>> r = requests.get('https://api.github.com/repos/psf/requests/issues/482')
>>> r.status_code
200

>>> issue = json.loads(r.text)

>>> print(issue[u'title'])
Feature any http verb in docs

>>> print(issue[u'comments'])
3

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

>>> r = requests.get(r.url + u'/comments')
>>> r.status_code
200

>>> comments = r.json()

>>> print(comments[0].keys())
[u'body', u'url', u'created_at', u'updated_at', u'user', u'id']

>>> print(comments[2][u'body'])
Probably in the "advanced" section

Отлично. Теперь давайте разместим комментарий, сообщающий этому постеру, что он глупый. Посмотрим кто он?

>>> print(comments[2][u'user'][u'login'])
kennethreitz

Хорошо, напишем этому парню из Кеннета, что мы считаем, что наш пример должен быть перемещен в краткое руководство. В соответствии с документацией API GitHub, наилучший способ сделать это — послать соответствующий запрос POST. Давай сделаем это.

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it!"})
>>> url = u"https://api.github.com/repos/psf/requests/issues/482/comments"

>>> r = requests.post(url=url, data=body)
>>> r.status_code
404

Так, у нас ничего не получилось. Нам, вероятно, еще нужно подтвердить подлинность нашего запроса. Это будет боль, верно? Нет. Requests упрощает использование многих форм аутентификации, включая очень распространенную базовую аутентификацию Basic Auth.

>>> from requests.auth import HTTPBasicAuth
>>> auth = HTTPBasicAuth('[email protected]', 'not_a_real_password')

>>> r = requests.post(url=url, data=body, auth=auth)
>>> r.status_code
201

>>> content = r.json()
>>> print(content[u'body'])
Sounds great! I'll get right on it.

Отлично. Но вот если бы я мог еще и редактировать этот комментарий! К счастью, GitHub позволяет нам использовать для этого HTTP метод PATCH. Давайте сделаем это.

>>> print(content[u"id"])
5804413

>>> body = json.dumps({u"body": u"Sounds great! I'll get right on it once I feed my cat."})
>>> url = u"https://api.github.com/repos/psf/requests/issues/comments/5804413"

>>> r = requests.patch(url=url, data=body, auth=auth)
>>> r.status_code
200

Отлично. Теперь, просто чтобы окончательно достать этого парня из Кеннета, я решил не говорить ему, что я работаю над этой проблемой. Это означает, что я хочу удалить свой последний комментарий. GitHub позволяет удалять комментарии, с использованием невероятно удачно названного метода DELETE. Давайте избавимся от него.

>>> r = requests.delete(url=url, auth=auth)
>>> r.status_code
204
>>> r.headers['status']
'204 No Content'

Отлично. Все комментарий пропал. И последнее, что если я захочу узнать какой лимит обращений к серверу я уже использовал (Rate Limit). Давайте разберемся с этим. GitHub отправляет эту информацию в заголовках, поэтому вместо загрузки всей страницы полностью я просто отправлю запрос HEAD для получения информации заголовков.

>>> r = requests.head(url=url, auth=auth)
>>> print(r.headers)
...
'x-ratelimit-remaining': '4995'
'x-ratelimit-limit': '5000'
...

Отлично. Пришло время написать скрипт на Python, который будет использовать API GitHub всеми возможными способами, еще 4995 раз 😉

Пользовательские методы запросов

В некоторых случаях вам будет необходимо работать с сервером, который по какой-либо причине разрешает или даже требует использования HTTP методов запросов, не описанных привычными спецификациями. Одним из примеров этого метод MKCOL, который используют некоторые серверы WEBDAV. Не волнуйтесь, вы также можете применять их с библиотекой Requests. Для этого используется метод request(). Например следующим образом:

>>> r = requests.request('MKCOL', url, data=data)
>>> r.status_code
200 # при условии, что ваш вызов будет корректен

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

Ссылки в заголовках запроса

Многие HTTP API содержат ссылки в заголовках запроса. Они делают API более самодокументируемыми и соответственно легко понимаемыми.

GitHub в своем API использует их для разбиения текстового содержимого на страницы (пагинация), например:

>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10'
>>> r = requests.head(url=url)
>>> r.headers['link']
'; rel="next", ; rel="last"'

Requests автоматически анализирует эти заголовки с ссылками и делает их использование в дальнейшем очень простым:

>>> r.links["next"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=2&per_page=10', 'rel': 'next'}

>>> r.links["last"]
{'url': 'https://api.github.com/users/kennethreitz/repos?page=7&per_page=10', 'rel': 'last'}

Транспортные адаптеры

Начиная с версии 1.0.0, Requests перешел на внутреннюю модульную архитектуру. Одной из причин, по которой это было сделано, была реализация транспортных адаптеров, первоначально описанных здесь . Транспортные адаптеры предоставляют механизм для определения методов взаимодействия со HTTP службами. В частности, они позволяют изменять конфигурацию запроса для взаимодействия с каждой службой.

Requests реализует один транспортный адаптер HTTPAdapter. Этот адаптер по умолчанию обеспечивает взаимодействие Requests с протоколами передачи данных HTTP и HTTPS, используя возможности мощной библиотеки urllib3 . Всякий раз, когда Requests инициализирует новый объект класса Session то, один из составляющих его объектов подключается к протоколу HTTP, а другой к HTTPS.

Requests позволяет пользователям создавать и использовать свои собственные транспортные адаптеры, которые предоставляют другие функциональные возможности. После создания пользовательского транспортного адаптера его можно подключить к объекту типа Session вместе с указанием веб-служб, к которым он должен применяться.

>>> s = requests.Session()
>>> s.mount('https://github.com/', MyAdapter())

Вызов метода монтирования регистрирует экземпляр класса транспортного адаптера для указанного префикса (подстроки URL). После подключения любой HTTP запрос, выполненный в текущем сеансе (сессии) URL которого начинается с указанного префикса, будет использовать наш транспортный адаптер MyAdapter().

Многие детали реализации транспортных адаптеров выходят за рамки этой документации. Далее мы рассмотрим пример простого сценария использования SSL, а также подклассов BaseAdapter.

Пример: использование определенной версии SSL

Команда, работающая над Requests ранее сделала особый выбор: какую по умолчанию использовать версию SSL, как базовую для библиотеки urllib3 . Время от времени вам может понадобиться подключиться к определенной точке входа сервиса (веб-службе), которая использует не совместимую со стандартной версию SSL.

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

import ssl
from urllib3.poolmanager import PoolManager

from requests.adapters import HTTPAdapter


class Ssl3HttpAdapter(HTTPAdapter):
    """"Transport adapter" that allows us to use SSLv3."""

    def init_poolmanager(self, connections, maxsize, block=False):
        self.poolmanager = PoolManager(
            num_pools=connections, maxsize=maxsize,
            block=block, ssl_version=ssl.PROTOCOL_SSLv3)

Блокирующий или неблокирующий?

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

Если же вас беспокоит вопрос блокировки ввода-вывода при загрузке и отправке данных, то существует множество проектов, которые объединяют функции Requests с одним из инструментов, реализующим асинхронность в Python. В качестве такого примера можно привести следующие расширения исходной библиотеки: requests-threads, grequests, requests-futures, и requests-async.

Порядок заголовков

В некоторых случаях необходимо передавать заголовки, формирующегося запроса, упорядоченными по какому-либо принципу. Если вы передаёте ваши заголовки запакованными в OrderedDict (словарей, сохраняющих порядок следования ключей) в именованный аргумент headers, то это должно обеспечить строгое упорядочение заголовков при передаче запроса. Однако порядок передаваемых заголовков по умолчанию, используемых библиотекой Requests, использовать предпочтительнее. Это вызвано тем, что если вы решите переопределить часть заголовков по умолчанию, используя параметр headers, то остальные заголовки, отправляемые при запросе, могут оказаться не в том порядке, который вам нужен.

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

Тайм-ауты соединений

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

Тайм-аут соединения — это число секунд в течение которых Requests будет ожидать пока клиент установит соединение с удаленной машиной (соответствует connect()) или сокетом. Рекомендуется устанавливать тайм-ауты подключения с продолжительностью чуть более 3 секунд, что соответствует длительности окна повторной передачи TCP-пакетов, устанавливаемой по умолчанию на сервере.

Время ожидания чтения (получения данных) это время, с момента когда ваш клиент подключится к серверу и отправит HTTP запрос, то есть это количество секунд в течение которых клиент будет ждать, пока сервер отправит ответ. В частности, это значение может содержать количество секунд, которое клиент будет ожидать между отдельными блоками байтов данных, отправляемых с сервера, а в 99,9% случаев это время до того, как сервер отправит первый байт.

Если вы указываете одно значение для тайм-аута, следующим образом:

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

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

r = requests.get('https://github.com', timeout=(3.05, 27))

Если удаленный сервер работает очень медленно, то вы можете попросить Requests бесконечно долго ждать его ответа, передав значение None в качестве значения аргумента timeout.

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