Utilisation d'APIs

Le World Wide Web regorge de données, encore faut-il savoir y accéder. Les APIs (Application Programming Interface) Web exposent des informations de natures diverses qui peuvent être extraites selon différents critères.

Une API Web se présente sous la forme d'une adresse internet (i.e. URL) et de suffixes (endpoints en anglais) pour chaque type de ressource accessible via l'API.

Afin d'illustrer cette notion d'interface de données, j'ai ponctué ce cours d'exemples progressifs en m'appuyant sur une API essentielle, à savoir The Internet Chuck Norris Database recensant les blagues relatives au légendaire Chuck Norris.

http://api.icndb.com/jokes/random

Dans l'exemple ci-dessus, http://api.icndb.com correspond à l'URL de l'API et /jokes/random est un des endpoints disponibles.

Le module requests

Ce module facilite les interactions avec une API Web en permettant de formuler des requêtes et d'exploiter les réponses retournées par le serveur.

Un module vient compléter le jeu d'instructions de base du langage Python. Il doit donc être installé avant d'être utilisé. La commande sous Windows est la suivante :

python.exe -m pip install requests

Et sous Linux ou macOS

python3 -m pip install requests

Attention !

Cette commande doit être exécutée en mode administrateur. Sous Windows, dans le menu Démarrer, clic droit sur l'application "invite de commandes" et choisir "exécuter en tant qu'administrateur".

Interrogation de l'API

Une API Web repose sur le protocole HTTP, un protocole de communication pour l'échange de données sur le Web. La fonction get() du module requests permet de formuler une requête HTTP en utilisant la méthode GET.

import requests

# Envoi d'une requête HTTP en méthode GET
response = requests.get('http://api.icndb.com/jokes/random')

Utilisation de la réponse

En mode synchrone, l'interprète Python attend la réponse de l'API avant de continuer l'exécution du script. Le résultat stocké dans la variable response peut ainsi être exploité dans la suite du code source.

Code d'état HTTP

import requests

response = requests.get('http://api.icndb.com/jokes/random')
            
print(response.status_code)

La variable response contient un attribut status_code qui correspond au code d'état HTTP retourné par le serveur.

Code Message
200 OK
403 Forbidden
404 Not Found
500 Internal Server Error
503 Service Unavailable

Ce code d'état permet de s'assurer que les données de la réponse ont bien été acheminées et, dans le cas contraire, de diagnostiquer le problème. La liste exhaustive des codes d'état HTTP est consultable en ligne.

Données au format JSON

JSON (JavaScript Object Notation) est une représentation de données arborescentes plus concise que le format XML. La requête

http://api.icndb.com/jokes/random

retourne la réponse ci-dessous

{
    "type": "success",
    "value":
    {
        "id": 211,
        "joke": "Nothing can escape the gravity of a black hole, except for Chuck Norris. Chuck Norris eats black holes. They taste like chicken.",
        "categories": []
    }
}

Les accolades { } délimitent un objet décrit par des couples clé/valeur. Dans notre exemple, la clé type a pour valeur success. Mais une valeur peut-être un objet d'où la nature arborescente (i.e. hiérarchique) des données représentées par le format JSON.

Une autre requête

http://api.icndb.com/jokes/random/3

retourne une réponse de la forme suivante

{
    "type": "success",
    "value":
    [
        {
            "id": 206,
            "joke": "Chuck Norris destroyed the periodic table, because Chuck Norris only recognizes the element of surprise.",
            "categories": []
        },
        {
            "id": 305,
            "joke": "Chuck Norris knows everything there is to know - Except for the definition of mercy.",
            "categories": []
        },
        {
            "id": 101,
            "joke": "Archaeologists unearthed an old english dictionary dating back to the year 1236. It defined "victim" as "one who has encountered Chuck Norris"",
            "categories": []
        }
    ]
}

Les crochets [ ] délimitent une liste de valeurs qui peuvent être des objets contenant des couples clé/valeur.

La méthode json() retourne le contenu JSON de la réponse sous la forme d'une variable de type dict, un dictionnaire Python basé sur le concept clé/valeur. L'accès à une valeur du dictionnaire se fait avec l'opérateur [ ] en précisant le nom de la clé associée.

import requests

response = requests.get('http://api.icndb.com/jokes/random')

if response.status_code == 200:
    data = response.json()
    
    print('\n\nJoke of the day:\n')
    print(data['value']['joke'], '\n')

Joke of the day:

An anagram for Walker Texas Ranger is KARATE WRANGLER SEX. I don't know what it is, but it sounds awesome.

Lorsque la réponse contient une liste d'objets, le code peut contenir une boucle pour parcourir l'ensemble des résultats.

import requests

response = requests.get('http://api.icndb.com/jokes/random/3')

if response.status_code == 200:
    data = response.json()

    print('\n\n', len(data['value']), 'jokes of the day:\n')

    for obj in data['value']:
        print(obj['joke'], '\n')

3 jokes of the day:

Chuck Norris ordered a Big Mac at Burger King, and got one.

There are two types of people in the world... people that suck, and Chuck Norris.

Jesus can walk on water, but Chuck Norris can walk on Jesus.

Requête HTTP avec paramètres

Une API Web autorise certains paramètres afin de filtrer les résultats. Ces paramètres répondent au même modèle clé/valeur. Dans notre exemple, ICNDb autorise les paramètres firstName et lastName afin de remplacer Chuck Norris par la personne de son choix.

import requests

parameters = {
    'firstName': 'Olivier',
    'lastName' : 'Nocent'
}

response = requests.get('http://api.icndb.com/jokes/random', params=parameters)

if response.status_code == 200:
    data = response.json()

    print('\n\nJoke of the day:\n')
    print(data['value']['joke'], '\n')

Joke of the day:

Olivier Nocent can touch MC Hammer.

Après cette brève introduction au fonctionnement des APIs Web, il est temps de développer vos propres projets. Pour démarrer, je vous recommande The Public API for Public APIs

balldontlie API

Je dois vous faire une confession : je n'ai pas conçu manuellement, ligne après ligne, les fichiers Excel de la saison NBA 2018-2019 du chapitre précédent. J'ai utilisé l'API balldontlie. Je vous propose de nous y attarder afin d'apprendre à interagir avec une API plus complexe qui propose des données paginées, à savoir des données volumineuses qui nécessitent plusieurs requêtes pour les consulter dans leur intégralité.

La section Get All Players de la documentation décrit l'utilisation du endpoint /players permettant de consulter l'ensemble des joueurs renseignés dans la base de données balldontlie.

Exercice

En étudiant la structure de la réponse retournée par le endpoint /players, écrivez un script Python affichant le nombre total de joueurs.

Correction
import requests

response = requests.get('https://www.balldontlie.io/api/v1/players')

if response.status_code == 200:
    data = response.json()
    
    print('NBA players count:', data['meta']['total_count'])

Les données, relativement volumineuses, sont retournées sous forme de pages dont la taille peut être définie par l'utilisateur grâce au paramètre per_page.

Exercice

Complétez votre script Python en demandant à récupérer 100 joueurs par page. Vous afficherez le nombre total de pages calculé par l'API.

Correction
import requests

parameters = {
  'per_page': 100
}

response = requests.get('https://www.balldontlie.io/api/v1/players', params=parameters)

if response.status_code == 200:
    data = response.json()
    
    print('NBA players count:', data['meta']['total_count'])
    print(data['meta']['total_pages'], 'pages')

Afin de récupérer l'intégralité des informations disponibles sur les joueurs, il est nécessaire d'interroger chacune des pages disponibles. Cette action peut être automatisée à l'aide d'une boucle qui met à jour une variable transmise à l'API via le paramètre page.

Exercice

Complétez à nouveau votre script Python afin d'intégrer un boucle qui effectuera une requête pour chaque page disponible. Les informations extraites de chaque réponse seront stockées dans 5 listes :

team
Abréviation du nom de l'équipe NBA
first_name
Prénom du joueur
last_name
Nom de famille du joueur
height
Taille, convertie en mètres, du joueur
weight
Poids, converti en kilogrammes, du joueur

Attention !

Certaines informations sont manquantes (valeur égale à None). Seuls les joueurs dont la taille et le poids sont disponibles seront ajoutés aux listes.
Correction
import requests

team       = []
first_name = []
last_name  = []
height     = []
weight     = []

parameters = {
    'per_page': 100
}

response = requests.get(
    'https://www.balldontlie.io/api/v1/players', params=parameters)

if response.status_code == 200:
    data = response.json()

    print('NBA players count:', data['meta']['total_count'])
    print(data['meta']['total_pages'], 'pages')

    for p in range(0, data['meta']['total_pages']):

        parameters = {
            'page': p,
            'per_page': 100
        }

        response = requests.get('https://www.balldontlie.io/api/v1/players', params=parameters)
        if response.status_code == 200:
            data = response.json()

            for player in data['data']:
                if player['height_feet'] != None and player['height_inches'] != None and player['weight_pounds'] != None:
                    team.append(player['team']['abbreviation'])
                    first_name.append(player['first_name'])
                    last_name.append(player['last_name'])
                    height.append(0.3048 * float(player['height_feet']) + 0.0254 * float(player['height_inches']))
                    weight.append(0.453592 * float(player['weight_pounds']))

Enfin, pour réaliser quelques statistiques simples, il est possible de remplir un objet DataFrame avec le contenu de 5 listes. Ce tableau peut aussi être sauvegardé au fomat CSV ou Excel.

Exercice

Complétez (une dernière fois, promis) votre script Python afin de créer un objet DataFrame pour calculer la moyenne et l'écart type des tailles et des poids des joueurs.

Vous sauvegarderez les données dans un fichier Excel players_stats.xlsx

Info

Il est possible du fusionner plusieurs listes afin de remplir un DataFrame :

df = pd.DataFrame(list(zip(list1, list2, list3)), columns=['Column1', 'Column2', 'Column3'])            
          
Correction
import requests
import pandas as pd

team       = []
first_name = []
last_name  = []
height     = []
weight     = []

parameters = {
    'per_page': 100
}

response = requests.get(
    'https://www.balldontlie.io/api/v1/players', params=parameters)

if response.status_code == 200:
    data = response.json()

    print('NBA players count:', data['meta']['total_count'])
    print(data['meta']['total_pages'], 'pages')

    for p in range(0, data['meta']['total_pages']):

        parameters = {
            'page': p,
            'per_page': 100
        }

        response = requests.get('https://www.balldontlie.io/api/v1/players', params=parameters)
        if response.status_code == 200:
            data = response.json()

            for player in data['data']:
                if player['height_feet'] != None and player['height_inches'] != None and player['weight_pounds'] != None:
                    team.append(player['team']['abbreviation'])
                    first_name.append(player['first_name'])
                    last_name.append(player['last_name'])
                    height.append(0.3048 * float(player['height_feet']) + 0.0254 * float(player['height_inches']))
                    weight.append(0.453592 * float(player['weight_pounds']))


df = pd.DataFrame(list(zip(team, last_name, first_name, height, weight)),
                  columns=['Team', 'Lastname', 'Firstname', 'Height', 'Weight'])


mean_height = df['Taille'].mean()
std_height = df['Taille'].std()

mean_weight = df['Poids'].mean()
std_weight = df['Poids'].std()

print('Height: ', mean_height, '-/+', std_height)
print('Weight: ', mean_weight, '-/+', std_weight)

# Enregistrement du DataFrame df au format Excel
writer = pd.ExcelWriter('data/NBA2018-2019/players_stats.xlsx', engine='xlsxwriter')
df.to_excel(writer, sheet_name='Anthropometrics', index=False)
writer.save()