Python и API: превосходное комбо для автоматизации работы с публичными данными


Использование API — один из тех «магических» навыков, которые открывают мир новых возможностей, а Python — отличный инструмент, чтобы таким навыком овладеть.

Материал представляет собой незначительно сокращенный перевод публикации Педро Прегейру Python & APIs: A Winning Combo for Reading Public Data. Перевод также адаптирован в виде блокнота Jupyter.

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

В этом туториале мы детально рассмотрим:

  1. Что такое API.
  2. Наиболее важные концепции, связанные с API.
  3. Как использовать Python для чтения данных, доступных через общедоступные API.

К концу прохождения туториала вы сможете использовать Python для большинства общедоступных API. Если вы разработчик, знание того, как использовать API-интерфейсы с Python, поможет в интеграции вашей работы со сторонними приложениями.ПримечаниеВ этом руководстве основное внимание уделяется тому, как использовать API-интерфейсы с помощью Python, но не их созданию. Для получения информации о создании API с помощью Python мы советуем обратиться к публикации API REST Python с Flask, Connexion и SQLAlchemy.

Знакомство с API

Освежите знания высшей математики для анализа данных с преподавателями из МГУВместе с нами. Запуск курса — 6 мартаЗаписаться

Аббревиатура API соответствует английскому application programming interface — программный интерфейс приложения. По сути, API действует как коммуникационный уровень или интерфейс, который позволяет различным системам взаимодействовать друг с другом без необходимости точно понимать, что делает каждая из систем.

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

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

В этом руководстве мы подробно остановимся на высокоуровневых веб-API, которые обмениваются информацией между сетями.

SOAP vs REST vs GraphQL

В конце 1990-х и начале 2000-х годов две разные модели дизайна API стали нормой для публичного доступа к данным:

  1. SOAP (Simple Object Access Protocol) ассоциируется с корпоративным миром, имеет строгую систему на основе «контрактов». Этот подход в основном связан скорее с обработкой действий, чем с данными.
  2. REST (Representational State Transfer) используется для общедоступных API и идеально подходит для получения данных из интернета.

Сегодня распространение также получает GraphQL — созданный Facebook гибкий язык API-запросов. Хотя GraphQL находится на подъеме и внедряется крупными компаниями, включая GitHub и Shopify, большинство общедоступных API-интерфейсов это REST API. Поэтому в рамках руководства мы ограничимся именно REST-подходом и тем, как взаимодействовать с такими API с помощью Python.

requests и API

При использовании API с Python нам понадобится всего одна библиотека: requests. С её помощью вы сможете выполнять бо́льшую часть, если не все, действия, необходимые для использования любого общедоступного API.

Установите библиотеку любым удобным вам способом, например, с помощью pip:

python3 -m pip install requests

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

Обращение к API с помощью Python

Достаточно разговоров — пора сделать первый вызов API! Мы вызовем популярный API для генерации случайных пользовательских данных. Единственное, что нужно знать для начала работы с API — по какому URL-адресу его вызывать. В этом примере это https://randomuser.me/api/, и вот самый простой вызов API, с которого мы и начнем:

import requests
requests.get("https://randomuser.me/api/")
<Response [200]>

Импортируем библиотеку requests, а затем получаем данные от URL-адреса. Мы еще не видим возвращенных данных, лишь результат запроса Response [200]. В терминах API такой результат означает, что всё прошло нормально.

Чтобы увидеть фактические данные, мы добавляем к имени переменной атрибут .text:

response = requests.get("https://randomuser.me/api/")
response.text
'{
    "results": [
    {
        "gender": "female",
        "name":
        {
            "title": "Mrs",
            "first": "Britt",
            "last": "Ludwig"
        },
        "location":
        {
            "street":
            {
                "number": 3409,
                "name": "Fasanenweg"
            },
            "city": "Emden",
            "state": "Mecklenburg-Vorpommern",
            "country": "Germany",
            "postcode": 81824,
            "coordinates":
            {
                "latitude": "-47.3424",
                "longitude": "28.1159"
            },
            "timezone":
            {
                "offset": "-12:00",
                "description": "Eniwetok, Kwajalein"
            }
        },
        "email": "britt.ludwig@example.com",
        "login":
        {
            "uuid": "28605437-cc28-4f66-995d-7a4f0d5b4540",
            "username": "smallkoala688",
            "password": "edthom",
            "salt": "VkG0ABwM",
            "md5": "d14e0101caa53d74f6f96a0cdee22f66",
            "sha1": "3abc70fe184d1ff5b41e96f6b4ae084658aedeea",
            "sha256": "f1a6e7cb7190624fe59ec8edc590a1e7defeaeb061637416581af032cda32bdf"
        },
        "dob":
        {
            "date": "1960-07-19T23:08:49.001Z",
            "age": 61
        },
        "registered":
        {
            "date": "2019-02-17T12:37:53.484Z",
            "age": 2
        },
        "phone": "0837-2320219",
        "cell": "0173-5245926",
        "id":
        {
            "name": "",
            "value": null
        },
        "picture":
        {
            "large": "https://randomuser.me/api/portraits/women/4.jpg",
            "medium": "https://randomuser.me/api/portraits/med/women/4.jpg",
            "thumbnail": "https://randomuser.me/api/portraits/thumb/women/4.jpg"
        },
        "nat": "DE"
    }],
    "info":
    {
        "seed": "03fb5e0d405fcad6",
        "results": 1,
        "page": 1,
        "version": "1.3"
    }
}'
    

Конечные точки и ресурсы

Как мы видели выше, первое, что нужно знать для использования API, — это базовый URL-адрес API. Вот так выглядят базовые URL-адреса нескольких известных провайдеров API:

Как видите, перечисленные URL начинаются с https:// api. Не существует определенного стандарта, но чаще всего базовый URL следует этому шаблону.

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

Сделаем запрос к интерфейсу TheDogAPI, аналогичный приведенному выше:

response = requests.get("https://api.thedogapi.com/")
response.text
'{"message":"The Dog API"}'

При вызове базового URL-адреса мы получаем сообщение, в котором говорится, что мы обратились к Dog API. Базовый URL здесь используется для получения информации об API, а не реальных данных.

Конечная точка (endpoint) — это часть URL-адреса, указывающая, какой ресурс мы хотим получить. Хорошо документированные API-интерфейсы содержат справочник по API, описывающий конечные точки и ресурсы API, а также способы их использования.

Есть такой справочник и у TheDogAPI. Попробуем обратиться к конечной точке, предоставляющей характеристики пород:

response = requests.get("https://api.thedogapi.com/v1/breeds")
response.text
'[{"weight":{"imperial":"6 - 13","metric":"3 - 6"},
"height":{"imperial":"9 - 11.5","metric":"23 - 29"},
"id":1,"name":"Affenpinscher","bred_for":"Small
rodent hunting, lapdog","breed_group":"Toy",
"life_span":"10 - 12 years","temperament":"Stubborn,
Curious, Playful, Adventurous, Active, Fun-loving",
"origin":"Germany, France","reference_image_id":
"BJa4kxc4X","image":{"id":"BJa4kxc4X","width":1600,
"height":1199,"url":
"https://cdn2.thedogapi.com/images/BJa4kxc4X.jpg"}},
{"weight": ...
    

Вуаля, мы получили список пород!

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

response = requests.get("https://api.thecatapi.com/v1/breeds")
response.text

Все взаимодействия между клиентом (в нашем случае консолью Python) и API разделены на запрос (request) и ответ (response):

Request и Response

  • request содержит данные запроса API: базовый URL, конечную точку, используемый метод, заголовки и т. д.
  • response содержит соответствующие данные, возвращаемые сервером, в том числе контент, код состояния и заголовки.

Снова обратившись к TheDogAPI, мы можем немного подробнее рассмотреть, что именно находится внутри объектов request и response:

response = requests.get("https://api.thedogapi.com/v1/breeds")
response
<Response [200]>
request = response.request
request
<PreparedRequest [GET]>
request.url
'https://api.thedogapi.com/v1/breeds'
request.path_url
'/v1/breeds'
request.method
'GET'
request.headers
{'User-Agent': 'python-requests/2.22.0',
'Accept-Encoding': 'gzip, deflate',
'Accept': '*/*', 'Connection': 'keep-alive'}
response
<Response [200]>
response.text
'[{"weight":{"imperial":"6 - 13","metric":"3 - 6"} ...
response.status_code
200
response.headers
{'Access-Control-Expose-Headers': ...
    

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

Коды состояний HTTP

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

Со временем вы без посторонней помощи научитесь распознавать различные коды состояний. Но пока приведем список наиболее распространенных:

Код состоянияОписание
200 OKЗапрос успешно выполнен.
201 CreatedЗапрос принят и создан ресурс.
400 Bad RequestЗапрос неверен или отсутствует некоторая информация.
401 UnauthorizedЗапрос требует дополнительных прав.
404 Not FoundЗапрошенный ресурс не существует.
405 Method Not AllowedКонечная точка не поддерживает этот метод HTTP.
500 Internal Server ErrorОшибка на стороне сервера.

Статус ответа можно проверить, используя .status_code и .reason. Библиотека requests также выводит код состояния в представлении Response-объекта:

response = requests.get("https://api.thedogapi.com/v1/breeds")
response
<Response [200]>
response.status_code
200
response.reason
'OK'

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

response = requests.get("https://api.thedogapi.com/v1/breedz")
response
<Response [404]>
response.status_code
404
response.reason
'Not Found'

Очевидно, конечной точки /breedz не существует, поэтому API возвращает код состояния 404 Not Found.

Заголовки HTTP

HTTP-заголовки (headers) используются для определения нескольких параметров, управляющих запросами и ответами:

HTTP HeaderОписание
AcceptКакой тип контента может принять клиент
Content-TypeКакой тип контента в ответе сервера
User-AgentКакое программное обеспечение клиент использует для связи с сервером
ServerКакое программное обеспечение сервер использует для связи с клиентом
AuthenticationКто вызывает API и с какими учетными данными

Чтобы проверить заголовки ответа, можно использовать response.headers:

response = requests.get("https://api.thedogapi.com/v1/breeds/1")
response.headers
{'Content-Encoding': 'gzip', 'Content-Type': 'application/json;
charset=utf-8', 'Date': 'Thu, 25 Feb 2021 05:17:40 GMT', 'Server':
'Apache/2.4.43 (Amazon)', 'Strict-Transport-Security':
'max-age=15552000; includeSubDomains', 'Vary':
'Origin,Accept-Encoding', 'X-Content-Type-Options':
'nosniff', 'X-DNS-Prefetch-Control': 'off', 'X-Download-Options':
'noopen', 'X-Frame-Options': 'SAMEORIGIN', 'X-Response-Time':
'1ms', 'X-XSS-Protection': '1; mode=block', 'Content-Length':
'265', 'Connection': 'keep-alive'}

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

response = requests.get("https://api.thedogapi.com/v1/breeds/1")
response.request.headers
{'User-Agent': 'python-requests/2.22.0', 'Accept-Encoding': 'gzip,
deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

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

Пользовательские заголовки

Еще один стандарт, с которым вы можете столкнуться при использовании API,— использование настраиваемых заголовков. Обычно они начинаются с префикса X-. Разработчики API обычно используют настраиваемые заголовки для отправки или запроса дополнительной информации от клиентов.

Для определения заголовков можно использовать словарь, передаваемый в метод requests.get(). Например, предположим, что вы хотите отправить некоторый идентификатор запроса на сервер API и знаете, что можете сделать это с помощью X-Request-Id:

headers = {"X-Request-Id": "<my-request-id>"}
response = requests.get("https://example.org", headers=headers)
response.request.headers
{'User-Agent': 'python-requests/2.22.0', 'Accept-Encoding':
'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive',
'X-Request-Id': '<my-request-id>'}

X-Request-Id находится среди других заголовков, которые по умолчанию идут с любым запросом API.

Ответ обычно содержит множество заголовков, но один из наиболее важных — Content-Type. Этот заголовок определяет тип содержимого, возвращаемого в ответе.

Content-Type

В наши дни большинство API-интерфейсов используют в качестве типа контента по умолчанию JSON.

Вернувшись к одному из предыдущих примеров использования TheDogAPI, мы заметим, что заголовок Content-Type определен как application/json:

response = requests.get("https://api.thedogapi.com/v1/breeds/1")
response.headers.get("Content-Type")
'application/json; charset=utf-8'

Помимо типа содержимого (в данном случае application/json), заголовок может возвращать кодировку контента

Вы можете столкнуться и c API, возвращающими XML или мультимедиа, например, изображения или видео.

Заголовок Content-Type позволяет узнать, как обрабатывать ответ и что делать с содержимым ответа.

Содержание ответа

Как мы только что узнали, тип контента указан в заголовке Content-Type ответа API. Чтобы правильно прочитать содержимое ответа в соответствии с различными заголовками Content-Type, объект Response поддерживает пару полезных атрибутов:

  • .text возвращает содержание ответа в формате юникод.
  • .content возвращает содержание ответа в виде байтовой строки.

Мы уже использовали выше атрибут .text. Но для некоторых типов данных, таких как изображения и другие нетекстовые данные, обычно лучшим подходом использование .content.

Для ответов API с типом содержимого application/json библиотека requests поддерживает специальный метод .json(), позволяющий получить представление данных в виде объекта Python:

response = requests.get("https://api.thedogapi.com/v1/breeds/1")
response.headers.get("Content-Type")
'application/json; charset=utf-8'
response.json()
{'weight': {'imperial': '6 - 13', 'metric': '3 - 6'},
 'height': {'imperial': '9 - 11.5', 'metric': '23 - 29'},
 'id': 1,
 'name': 'Affenpinscher',
 'bred_for': 'Small rodent hunting, lapdog',
 'breed_group': 'Toy',
 'life_span': '10 - 12 years',
 'temperament': 'Stubborn, Curious, Playful, Adventurous, Active, Fun-loving',
 'origin': 'Germany, France',
 'reference_image_id': 'BJa4kxc4X'}
response.json()["name"]
'Affenpinscher'

Как видите, после выполнения response.json() мы получаем словарь, который можно использовать так же, как любой другой словарь в Python.

Методы HTTP

При вызове API существует несколько различных методов, которые мы можем использовать, чтобы указать, какое действие хотим выполнить. Например, если мы хотим получить некоторые данные, мы используем метод GET, а если нужно создать некоторые данные — метод POST.

Вот список наиболее распространенных методов и их типичных вариантов использования:

HTTP-методОписаниеМетод requests
POSTСоздает новый ресурс.requests.post()
GETСчитывает имеющийся ресурс.requests.get()
PUTОбновляет существующий ресурс.requests.put()
DELETEУдаляет ресурс.requests.delete()

Эти четыре метода также называют CRUD-операциями, поскольку они позволяют создавать (create), читать (read), обновлять (update) и удалять (delete) ресурсы.

До сих пор мы использовали только .get(), но мы можем использовать requests для всех прочих HTTP-методов:

requests.post("https://api.thedogapi.com/v1/breeds/1")
requests.get("https://api.thedogapi.com/v1/breeds/1")
requests.put("https://api.thedogapi.com/v1/breeds/1")
requests.delete("https://api.thedogapi.com/v1/breeds/1")

Большинство этих запросов вернут код состояния 405 (Method Not Allowed). Не все конечные точки поддерживают методы POST, PUT или DELETE. Действительно, большинство общедоступных API разрешают только запросы GET и не позволяют создавать или изменять существующие данные без авторизации.

Параметры запроса

Иногда при вызове API можно получить тонну данных, которые в массе своей не нужны. При вызове конечной точки TheDogAPI/breeds мы получаем всю информацию о каждой породе, но вполне вероятно, что нам достаточно лишь небольшой части данных для одного подвида собак. Тут пригождаются параметры запроса!

Наверняка вы уже сталкивались с параметрами запроса при просмотре веб-страниц в Интернете. При просмотре видео на YouTube у вас есть URL-адрес вида https://www.youtube.com/watch?v=aL5GK2LVMWI. Параметр v= в URL-адресе и есть параметр запроса. Обычно он идет после базового URL-адреса и конечной точки.

Чтобы добавить параметр запроса к заданному URL-адресу, мы должны добавить вопросительный знак (?) перед первым параметром запроса. Если в запросе нужно указать несколько параметров, их разделяют с помощью амперсанда (&).

Тот же URL-адрес YouTube, указанный выше, с несколькими параметрами запроса будет выглядеть следующим образом: https://www.youtube.com/watch?v=aL5GK2LVMWI&t=75.

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

Возвратимся к API генератора случайных пользователей:

requests.get("https://randomuser.me/api/").json()
{'results': [{'gender': 'female',
   'name': {'title': 'Mrs', 'first': 'Georgia', 'last': 'Hamilton'},
   'location': {'street': {'number': 7475, 'name': 'Elgin St'},
    'city': 'Fort Lauderdale',
    'state': 'North Carolina',
    'country': 'United States',
    'postcode': 52323,
    'coordinates': {'latitude': '83.6943', 'longitude': '111.3404'},
    'timezone': {'offset': '-9:00', 'description': 'Alaska'}},
   'email': 'georgia.hamilton@example.com',
   'login': {'uuid': '5571b45a-739e-4bd2-a378-3f74da397fcc',
    'username': 'orangeduck951',
    'password': 'impala',
    'salt': 'pK6TZkNp',
    'md5': 'b58f48c5f58d0d33c6c958512bea6900',
    'sha1': '2336e82d79d64e8668fb3f92126dedda14ce1f53',
    'sha256': '9234f819158c8473e175732db22da52a0aab9ab4c636c3fec32cad471319f492'},
   'dob': {'date': '1969-06-21T00:54:25.658Z', 'age': 52},
   'registered': {'date': '2007-02-07T08:51:37.427Z', 'age': 14},
   'phone': '(625)-497-7824',
   'cell': '(253)-658-6904',
   'id': {'name': 'SSN', 'value': '959-66-1235'},
   'picture': {'large': 'https://randomuser.me/api/portraits/women/84.jpg',
    'medium': 'https://randomuser.me/api/portraits/med/women/84.jpg',
    'thumbnail': 'https://randomuser.me/api/portraits/thumb/women/84.jpg'},
   'nat': 'US'}],
 'info': {'seed': 'b5a5ac332d8ef9b4',
  'results': 1,
  'page': 1,
  'version': '1.3'}}
    

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

requests.get("https://randomuser.me/api/?gender=female&nat=de").json()
{'results': [{'gender': 'female',
   'name': {'title': 'Ms', 'first': 'Sylvia', 'last': 'Kranz'},
   'location': {'street': {'number': 1035, 'name': 'Mühlenstraße'},
    'city': 'Neckarsulm',
    'state': 'Niedersachsen',
    'country': 'Germany',
    'postcode': 92153,
    'coordinates': {'latitude': '-81.2409', 'longitude': '147.2697'},
    'timezone': {'offset': '-10:00', 'description': 'Hawaii'}},
   'email': 'sylvia.kranz@example.com',
   'login': {'uuid': '7f11b98e-91de-42ac-a622-3aec488ab07c',
    'username': 'blackcat302',
    'password': 'gggggggg',
    'salt': 'Je25kPW6',
    'md5': '7b6404e608b123da0cf32dbb69d19f4b',
    'sha1': '0806c0dbd9abf57b863b40da1a08cf322da2d404',
    'sha256': '3bccd7b925d2753350188f43f5fe5e02530eb322f54434b7280e39ca332c04d6'},
   'dob': {'date': '1979-10-01T01:44:21.871Z', 'age': 42},
   'registered': {'date': '2018-02-11T11:41:41.773Z', 'age': 3},
   'phone': '0355-6704793',
   'cell': '0172-9785781',
   'id': {'name': '', 'value': None},
   'picture': {'large': 'https://randomuser.me/api/portraits/women/72.jpg',
    'medium': 'https://randomuser.me/api/portraits/med/women/72.jpg',
    'thumbnail': 'https://randomuser.me/api/portraits/thumb/women/72.jpg'},
   'nat': 'DE'}],
 'info': {'seed': 'ab7997046a8ce34d',
  'results': 1,
  'page': 1,
  'version': '1.3'}}   

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

Чтобы избежать повторного создания URL-адреса, мы можем передавать параметры запроса в виде атрибута-словаря params:

query_params = {"gender": "female", "nat": "de"}
requests.get("https://randomuser.me/api/", params=query_params).json()
{'results': [{'gender': 'female',
   'name': {'title': 'Mrs', 'first': 'Heide-Marie', 'last': 'Liebert'},
   'location': {'street': {'number': 481, 'name': 'Wiesenstraße'},
    'city': 'Barby',
    'state': 'Berlin',
    'country': 'Germany',
    'postcode': 44617,
    'coordinates': {'latitude': '71.1713', 'longitude': '-35.9063'},
    'timezone': {'offset': '-4:00',
     'description': 'Atlantic Time (Canada), Caracas, La Paz'}},
   'email': 'heide-marie.liebert@example.com',
   'login': {'uuid': '8840f126-71f8-4107-a58c-6f76255ddbbb',
    'username': 'orangerabbit454',
    'password': 'ffffff',
    'salt': 'Cg8FgcCP',
    'md5': '4095dbd532bd0cf11b5c98e523047966',
    'sha1': 'abb397200399b655841e704be8c06c6f023d24e5',
    'sha256': '7c8d10d84e1a2c0a6da756547cba3411f243df88b66ac16f457eb79a823a1136'},
   'dob': {'date': '1996-08-05T09:49:34.663Z', 'age': 25},
   'registered': {'date': '2010-01-27T02:30:33.015Z', 'age': 11},
   'phone': '0133-8876656',
   'cell': '0172-0931163',
   'id': {'name': '', 'value': None},
   'picture': {'large': 'https://randomuser.me/api/portraits/women/37.jpg',
    'medium': 'https://randomuser.me/api/portraits/med/women/37.jpg',
    'thumbnail': 'https://randomuser.me/api/portraits/thumb/women/37.jpg'},
   'nat': 'DE'}],
 'info': {'seed': 'faa5213e18a7e753',
  'results': 1,
  'page': 1,
  'version': '1.3'}}

Подход можно применить к любому другому API, в документации которого описаны параметры запроса. Например, TheDogAPI позволяет отфильтровать конечную точку /breeds, чтобы вернуть породы, соответствующие определенному имени. Например, если мы хотим найти породу Лабрадудель, мы можем сделать это с параметром запроса q:

query_params = {"q": "labradoodle"}
endpoint = "https://api.thedogapi.com/v1/breeds/search"
requests.get(endpoint, params=query_params).json()
[{'weight': {'imperial': '45 - 100', 'metric': '20 - 45'},
  'height': {'imperial': '14 - 24', 'metric': '36 - 61'},
  'id': 148,
  'name': 'Labradoodle',
  'breed_group': 'Mixed',
  'life_span': '10 - 15 years'}] 

Изучение продвинутых концепций API

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

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

Хотя многие API бесплатны и полностью общедоступны, аутентификация обычно существенно расширяет права доступа. Существует множество API, требующих аутентификации, например:

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

Как правило, вызов API без учетных данных или с некорректной учетной записью возвращают коды состояний 401 Unauthorized или 403 Forbidden.

Ключи API

Самый распространенный подход к аутентификации — это ключ API (API key). Эти ключи используются для идентификации вас как пользователя или клиента API, а также для отслеживания использования вами интерфейса. Ключи API обычно отправляются как заголовок запроса или как параметр запроса.

В этом примере мы воспользуемся API-интерфейсом NASA Mars Rover Photo API и получим снимки, сделанные 1 июля 2020 года. В целях тестирования вы можете использовать ключ API DEMO_KEY, который НАСА предоставляет по умолчанию. В противном случае вы можете быстро создать собственный, перейдя на главную страницу API и нажав Get Started.

Чтобы добавить в свой запрос ключ API, укажите параметр запроса api_key=.

endpoint = "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos"
# Замените DEMO_KEY ниже своим собственным ключом, если вы его сгенерировали.
api_key = "DEMO_KEY"
query_params = {"api_key": api_key, "earth_date": "2020-07-01"}
response = requests.get(endpoint, params=query_params)
response
<Response [200]> 

Всё идет нормально. Нам удалось сделать аутентифицированный запрос к API NASA и получить ответ 200 OK.

Взглянем поближе на объект Response и попробуем извлечь из него несколько изображений:

response.json()
{'photos': [{'id': 754118,
   'sol': 2809,
   'camera': {'id': 20,
    'name': 'FHAZ',
    'rover_id': 5,
    'full_name': 'Front Hazard Avoidance Camera'},
   'img_src': 'https://mars.nasa.gov/msl-raw-images/proj/msl/redops/ods/surface/sol/02809/opgs/edr/fcam/FLB_646868981EDR_F0810628FHAZ00337M_.JPG',
   'earth_date': '2020-07-01',
   'rover': {'id': 5,
    'name': 'Curiosity',
    'landing_date': '2012-08-06',
    'launch_date': '2011-11-26',
    'status': 'active'}},
...
    
photos = response.json()["photos"]
print(f"Найдено {len(photos)} фотографий.")
Найдено 12 фотографий.
photos[7]["img_src"]
'https://mars.nasa.gov/msl-raw-images/proj/msl/redops/ods/surface/sol/02809/opgs/edr/rcam/RLB_646860185EDR_F0810628RHAZ00337M_.JPG'

Мы используем .json() для преобразования ответа в словарь Python, затем извлекаем поле photos и получаем URL-адрес изображения для одной из фотографий. Если мы откроем URL в браузере, то увидим снимок Марса, сделанный марсоходом Curiosity:

OAuth: начало работы

Другой распространенный стандарт аутентификации API — это OAuth. Это очень обширная тема, поэтому мы коснемся только самых основ.

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

Вот пошаговое описание того, что происходит, когда мы нажимаем в приложении Spotify кнопку «Продолжить с Facebook»:

  1. Приложение Spotify запрашивает API Facebook запустить процесс аутентификации. Для этого приложение Spotify отправит идентификатор приложения (client_id) и URL-адрес (redirect_uri) для перенаправления пользователя после взаимодействия с API Facebook.
  2. Клиент будет перенаправлен на сайт Facebook, где нас попросят войти в систему с учетными данными. Приложение Spotify не увидит эти учетные данные и не получит к ним доступа. Это самое важное преимущество OAuth.
  3. Facebook отобразит данные профиля, запрашиваемые приложением Spotify, и попросит принять или отклонить обмен этими данными.
  4. Если вы согласитесь предоставить Spotify доступ к своим данным, вы будете перенаправлены обратно в приложение Spotify и получите доступ к системе.

При прохождении четвертого шага Facebook предоставит Spotify специальные учетные данные — токен доступа (access_token), который можно многократно использовать для получения информации. Этот токен входа в Facebook действителен в течение шестидесяти дней, но у других приложений могут быть другие сроки действия.

С технической точки зрения вот что нам нужно знать при использовании API с использованием OAuth:

  • Нам нужно создать приложение, которое будет иметь идентификатор (app_id или client_id) и некоторую секретную строку (app_secret или client_secret).
  • У нас должен быть URL-адрес перенаправления (redirect_uri), который API будет использовать для отправки нам информации.
  • В результате аутентификации мы получим код (exchange_code), который необходимо обменять на токен доступа (access_token).

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

OAuth: практический пример

Как мы видели выше, первое, с чего стоит начать — создать приложение. В документации GitHub есть отличное пошаговое объяснение, как это сделать. Чтобы не разворачивать отдельный сервер, в качестве адреса для перенаправления можно использовать адрес https://httpbin.org/anything. Эта веб-страница просто выводит все, что получает на входе.

Создадим приложение, скопируем и вставим Client_ID и Client_Secret вместе с указанным URL для переадресации в файл Python, который назовем github.py:

import requests

# Замените следующие переменные вашим Client ID и Client Secret
CLIENT_ID = "<REPLACE_WITH_CLIENT_ID>"
CLIENT_SECRET = "<REPLACE_WITH_CLIENT_SECRET>"

# Замените значение переменной с помощью url, указанного вами
# в поле "Authorization callback URL"
REDIRECT_URI = "<REPLACE_WITH_REDIRECT_URI>"

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

def create_oauth_link():
    params = {
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "user",
        "response_type": "code",
    }

endpoint = "https://github.com/login/oauth/authorize"
response = requests.get(endpoint, params=params)
url = response.url
return url
    

Сначала мы определяем требуемые параметры, которые ожидает API, а затем вызываем API, используя requests.get().

Когда мы делаем запрос к конечной точке /login/oauth/ authorize, API автоматически перенаправляет нас на сайт GitHub. В этом случае мы хотим получить из ответа параметр url. Этот параметр содержит точный URL-адрес, на который GitHub нас перенаправляет.

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

def exchange_code_for_access_token(code=None):
    params = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": REDIRECT_URI,
        "code": code,
    }

headers = {"Accept": "application/json"}
endpoint = "https://github.com/login/oauth/access_token"
response = requests.post(endpoint, params=params, headers=headers).json()
return response["access_token"]
    

Здесь мы делаем POST-запрос для обмена кода на токен доступа. В запросе мы должны отправить CLIENT_SECRET и код, чтобы GitHub проверил, что код сгенерирован нашим приложением. После этого GitHub API генерирует и возвращает токен доступа.

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

link = create_oauth_link()
print(f"Перейдите по ссылке, чтобы запустить аутентификацию с помощью GitHub: {link}")
code = input("GitHub code: ")
access_token = exchange_code_for_access_token(code)
print(f"Exchanged code {code} и access token: {access_token}")

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

Попробуем добавить следующий код, чтобы получить свой профиль пользователя с помощью User API и распечатать свое имя, имя пользователя и количество приватных репозиториев:

def print_user_info(access_token=None):
    headers = {"Authorization": f"token {access_token}"}
    endpoint = "https://api.github.com/user"
    response = requests.get(endpoint, headers=headers).json()
    name = response["name"]
    username = response["login"]
    private_repos_count = response["total_private_repos"]
    print(
        f"{name} ({username}) | private repositories: {private_repos_count}"
    )

Теперь, когда у нас есть токен доступа, необходимо отправлять его со всеми запросам API в заголовке Authorization. Ответом на запрос будет словарь Python, содержащий информацию о пользователе. Из этого словаря мы хотите получить поля namelogin и total_private_repos. Мы также можете распечатать переменную respinse, чтобы увидеть, какие еще поля доступны.

Осталось только собрать все вместе и попробовать:github.py

import requests

# Замените следующие переменные вашим Client ID и Client Secret
CLIENT_ID = "<REPLACE_WITH_CLIENT_ID>"
CLIENT_SECRET = "<REPLACE_WITH_CLIENT_SECRET>"

# Замените значение переменной с помощью url, указанного вами
# в поле "Authorization callback URL"
REDIRECT_URI = "<REPLACE_WITH_REDIRECT_URI>"


def create_oauth_link():
    params = {
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "user",
        "response_type": "code",
    }
endpoint = "https://github.com/login/oauth/authorize"
response = requests.get(endpoint, params=params)
url = response.url
return url


def exchange_code_for_access_token(code=None):
    params = {
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": REDIRECT_URI,
        "code": code,
    }
headers = {"Accept": "application/json"}
endpoint = "https://github.com/login/oauth/access_token"
response = requests.post(endpoint, params=params, headers=headers).json()
return response["access_token"]


def print_user_info(access_token=None):
    headers = {"Authorization": f"token {access_token}"}
    endpoint = "https://api.github.com/user"
    response = requests.get(endpoint, headers=headers).json()
    name = response["name"]
    username = response["login"]
    private_repos_count = response["total_private_repos"]
    print(
        f"{name} ({username}) | private repositories: {private_repos_count}"
    )

link = create_oauth_link()
print(f"Follow the link to start the authentication with GitHub: {link}")
code = input("GitHub code: ")
access_token = exchange_code_for_access_token(code)
print(f"Exchanged code {code} и access token: {access_token}")
print_user_info(access_token=access_token)
    

В результате запуска скрипта мы получим примерно такой результат:

John Doe (johndoe) | number of private repositories: 42
    

Большинство API-интерфейсов, использующих OAuth, ведут себя одинаково, поэтому достаточно один раз разобраться во всех процессах.

Пагинация

За пересылку большого массива данных между клиентами и сервером приходится платить пропускной способностью. Для снижения нагрузки на сервер API-интерфейсы обычно используют пагинацию — разбиение выдаваемой информации на страницы.

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

В API пагинация обычно обрабатывается с помощью двух параметров запроса:

  1. Атрибут page определяет номер запрашиваемой таблицы
  2. Атрибут size определяет размер каждой страницы

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

Снова воспользуемся GitHub API. Параметр per_page= определяет количество возвращаемых элементов, а page= позволяет разбивать результат на отдельные страницы. Пример использования параметров:

response = requests.get("https://api.github.com/events?per_page=1&page=0")
response.json()[0]["id"]
'15315291644'
response = requests.get("https://api.github.com/events?per_page=1&page=1")
response.json()[0]["id"]
'15316180831'
response = requests.get("https://api.github.com/events?per_page=1&page=2")
response.json()[0]["id"]
'15316181822'
    

Используя параметр запроса page=, мы получаем страницы без перегрузки API.

Ограничение скорости

Учитывая, что рассматриваемые API-интерфейсы являются общедоступными и могут использоваться кем угодно, ими пытаются злоупотреблять люди с плохими намерениями. Чтобы предотвратить такие атаки, используется метод, называемый ограничением скорости (rate limit). API ограничивает количество запросов, которые пользователи могут сделать за определенный период. В случае превышения лимита API-интерфейсы временно блокируют IP-адрес или API-ключ.

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

Использование API с помощью Python: практические примеры

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

Запрос наиболее популярных сейчас гифок

Как насчет создания небольшого скрипта, который извлекает три самых популярных сейчас GIF-файла с веб-сайта GIPHY? Начните с получения API-ключа:

  1. Создайте аккаунт на GIPHY
  2. Перейдите в панель разработчика и зарегистрируйте новое приложение.
  3. Получите ключ для соединения с API.

Ключ API используем в GIPHY API:

import requests

API_KEY = "API_KEY"
endpoint = "https://api.giphy.com/v1/gifs/trending"

params = {"api_key": API_KEY, "limit": 3, "rating": "g"}
response = requests.get(ENDPOINT, params=params).json()
for gif in response["data"]:
    title = gif["title"]
    trending_date = gif["trending_datetime"]
    url = gif["url"]
    print(f"{title} | {trending_date} | {url}")
    

Запуск этого кода выведет структурированный список со ссылками на гифки:

Excited Schitts Creek GIF by CBC | 2020-11-28 20:45:14 | https://giphy.com/gifs/cbc-schittscreek-schitts-creek-SiGg4zSmwmbafTYwpj
Saved By The Bell Shrug GIF by PeacockTV | 2020-11-28 20:30:15 | https://giphy.com/gifs/peacocktv-saved-by-the-bell-bayside-high-school-dZRjehRpivtJsNUxW9
Schitts Creek Thank You GIF by CBC | 2020-11-28 20:15:07 | https://giphy.com/gifs/cbc-funny-comedy-26n79l9afmfm1POjC
    

Получение подтвержденных случаев COVID-19 в каждой стране

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

import requests
from datetime import date, timedelta

today = date.today()
yesterday = today - timedelta(days=1)
country = "Russia"
endpoint = f"https://api.covid19api.com/country/{country}/status/confirmed"
params = {"from": str(yesterday), "to": str(today)}

response = requests.get(endpoint, params=params).json()
total_confirmed = 0
for day in response:
    cases = day.get("Cases", 0)
    total_confirmed += cases

print(f"Total Confirmed Covid-19 cases in {country}: {total_confirmed}")
    
Total Confirmed Covid-19 cases in Russia: 4153735

    

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

Поиск в Google Книгах

Воспользуемся API Google Книг для поиска информации об интересующей нас книге. Вот простой фрагмент кода для поиска названия книги Моби Дик во всем каталоге с выдачей трех первых записей:

import requests

endpoint = "https://www.googleapis.com/books/v1/volumes"
query = "Моби Дик"

params = {"q": query, "maxResults": 3}
response = requests.get(endpoint, params=params).json()
for book in response["items"]:
    volume = book["volumeInfo"]
    title = volume["title"]
    published = volume.get("publishedDate", "год издания неизвестен")
    description = volume.get("description", "описание отсутствует")
    print(f"{title} ({published}) | {description}")
    
Моби Дик (год издания неизвестен) | «Моби Дик» — самый
известный роман американского писателя Германа Мелвилла
(1819–1891), романтика, путешественника, философа, поэта,
автора морских повестей и психологических рассказов. В
настоящем издании «Моби Дик»...
Моби Дик (2018-01-03) | Моби Дик — это не кит, это человек…
Он одинок и у него нет никого и ничего, кроме работы,
составляющей всю его жизнь. И лишь настоящие чувства,
пробужденные в нем девушкой, изменяют смысл его жизни...
Моби Дик (1961) | описание отсутствует
    

Вы можете использовать свои знания OAuth и создать приложение, хранящее записи о книгах, которые читаете или хотите прочитать.

Заключение

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

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

Источники