Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout

fetch the Internet#

JSON#

commençons par une donnée en JSON; par exemple ceci samayo/country-json
pour trouver le bon lien, je clique sur ‘Raw’ et je copie ici

URL1 = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-population.json"

Il s’agit pour nous d’écrire un code qui va chercher cette donnée et les traduit en une structure Python (à base de list et dict) -
la structure en question est déjà présente dans le JSON.

Indices sur la librairie requests#

ce module ne fait pas partie de la librairie standard, il faut l’installer avec .. wait for it

pip install requests

(il y a des fonctions de ce genre dans la librairie standard, mais c’est celle-ci qui est le plus souvent utilisée)

poster la requête#

import json

import requests
# il faut être un peu patient, surtout si votre réseau est un peu lent

response = requests.get(URL1)

les attributs de l’objet response#

# dans ce genre de cas dir() est utile
# sauf que c'est un peu trop bavard
# dir(response)
# du coup ça vaut le coup de filter un peu

[symbol for symbol in dir(response) if not symbol.startswith('_')]
['apparent_encoding',
 'close',
 'connection',
 'content',
 'cookies',
 'elapsed',
 'encoding',
 'headers',
 'history',
 'is_permanent_redirect',
 'is_redirect',
 'iter_content',
 'iter_lines',
 'json',
 'links',
 'next',
 'ok',
 'raise_for_status',
 'raw',
 'reason',
 'request',
 'status_code',
 'text',
 'url']
# si tout s'est bien passé ici on doit avoir 200
# (pour être précis: entre 200 et 299)
# c'est un des codes de retours de HTTP
# si c'est par exemple 404 ça signifie que cette URL n'existe plus

response.status_code
200
# si on était curieux on pourrait aussi faire

response.headers
{'Connection': 'keep-alive', 'Content-Length': '3197', 'Cache-Control': 'max-age=300', 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; sandbox", 'Content-Type': 'text/plain; charset=utf-8', 'ETag': 'W/"81c9e62269c8660e7bf10f234993c94c4ea4dc2b725e4bd5f2df78505cab71e7"', 'Strict-Transport-Security': 'max-age=31536000', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'deny', 'X-XSS-Protection': '1; mode=block', 'X-GitHub-Request-Id': 'E123:6F396:F9438:138C6E:68CEABCC', 'Content-Encoding': 'gzip', 'Accept-Ranges': 'bytes', 'Date': 'Sat, 20 Sep 2025 13:27:40 GMT', 'Via': '1.1 varnish', 'X-Served-By': 'cache-cmh1290096-CMH', 'X-Cache': 'MISS', 'X-Cache-Hits': '0', 'X-Timer': 'S1758374861.719623,VS0,VE157', 'Vary': 'Authorization,Accept-Encoding', 'Access-Control-Allow-Origin': '*', 'Cross-Origin-Resource-Policy': 'cross-origin', 'X-Fastly-Request-ID': 'aae6fda7e19ec85c22eab31746a0687c208ab6a9', 'Expires': 'Sat, 20 Sep 2025 13:32:40 GMT', 'Source-Age': '0'}
# pour voir par exemple la taille de notre donnée

response.headers['Content-Length']
'3197'
# mais bon, bien sûr ce qu'on veut surtout c'est le contenu
# c'est une chaine, regardons les 100 premiers caractères

print(type(response.text))
print(response.text[:100], '...')
<class 'str'>
[
    {
        "country": "Afghanistan",
        "population": 37172386
    },
    {
        "count ...

indices sur la librairie json#

pour le coup, elle est dans la stdlib; les deux méthodes intéressantes sont

  • json.loads(json_string) qui décode une chaine JSON en un objet Python, et

  • json.dumps(object) qui encode un object Python en une chaine JSON

le code#

du coup le code que je dois écrire pour faire le job est simplement

def get_url_as_json(url):
    """
    Fetch a URL and decode its result as JSON
    """

    with requests.get(url) as response:
        return json.loads(response.text)
python_friendly = get_url_as_json(URL1)

# on obtient quoi comme type ?
type(python_friendly)
list
python_friendly[:3]
[{'country': 'Afghanistan', 'population': 37172386},
 {'country': 'Albania', 'population': 2866376},
 {'country': 'Algeria', 'population': 42228429}]
python_friendly[-3:]
[{'country': 'Yemen', 'population': 28498687},
 {'country': 'Zambia', 'population': 17351822},
 {'country': 'Zimbabwe', 'population': 14439018}]
len(python_friendly)
244

option: transformer la structure#

# et ici par exemple je pourrais décider que c'est plus pratique
# sous la forme d'un dictionnaire name -> population

population = {d['country']: d['population'] for d in python_friendly}
# de sorte que je peux faire simplement
population['France']
66977107

CSV#

Vous trouverez à cette URL un accès aux données de population par pays datasets/population

Attention

ce ne sont pas les mêmes données exactement, celles-ci sont beaucoup plus détaillées…

# pareil, on clique sur 'Raw' pour obtenir le lien vers la donnée 'crue'

URL2 = "https://raw.githubusercontent.com/datasets/population/main/data/population.csv"
# il faut être un peu patient car c'est bcp + gros que tout à l'heure

response = requests.get(URL2)
response.headers['Content-Length']
'130037'

indices sur la librairie csv#

# il semble donc qu'on ait affaire à un csv classique
# qu'on peut décortiquer avec object csv.reader
# https://docs.python.org/3/library/csv.html

import csv

il se trouve que la librairie csv fournit un objet csv.reader qui prend un paramètre de type file
c’est-à-dire comme par exemple le résultat de open()

ça demande une petite gymnastique, car nous avons le texte en mémoire, et pas dans un fichier…

option 1 - pas beau !#

très inélégant, mais qui peut marcher à la rigueur:
j’ai une chaine, je l’écris dans un fichier, que je peux ensuite rouvrir en lecture pour le passer à csv.reader
une option viable en dernier recours, mais on ne va pas le faire, car c’est très vilain, et il y a une astuce pour s’en sortir autrement:

option 2 - mieux#

exactement pour contourner ce genre de cas, il y a une classe io.StringIO qui, en partant d’une chaine en mémoire, se comporte comme un fichier ! et donc on va utiliser cela :

# pour utiliser csv.reader, il nous faut un objet de type fichier (file-like object)
# et c'est là que StringIO est super pratique

from io import StringIO

with (StringIO(response.text)) as file_like:
    reader = csv.reader(file_like, delimiter=',')
    # on regarde juste les 3 premiers enregistrements
    for i, row in enumerate(reader, 1):
        print(i, row)
        if i >= 3:
            break
1 ['Country Name', 'Country Code', 'Year', 'Value']
2 ['Aruba', 'ABW', '1960', '54922']
3 ['Aruba', 'ABW', '1961', '55578']

À ce stade, on voit comment on peut transformer nos données en listes Python; mais on va s’arrêter là, parce qu’en pratique ça c’est vraiment un exercice qu’on ferait en pandas…

un aperçu de pandas#

pandas est une librairie externe aussi, qu’il faut absolument connaitre un peu, dès qu’on manipule de la data
et pour commencer il y a une fonction read_csv qui va faire le job complet (acquisition et chargement) en un seul appel:

import pandas as pd
# en vrai on fait comme ça: être patient .. à nouveau

df = pd.read_csv(URL2)
# on regarde un peu ce qu'il y a dedans

df.head(3)

# on peut aussi regarder la fin avec df.tail(3)
Country Name Country Code Year Value
0 Aruba ABW 1960 54922.0
1 Aruba ABW 1961 55578.0
2 Aruba ABW 1962 56320.0
# conversion en date

df['time'] = pd.to_datetime(df.Year, format="%Y")
df.head(3)
Country Name Country Code Year Value time
0 Aruba ABW 1960 54922.0 1960-01-01
1 Aruba ABW 1961 55578.0 1961-01-01
2 Aruba ABW 1962 56320.0 1962-01-01
# on nettoie / renomme

del df['Year']

df = df.rename(columns = {
    'Country Name': 'country',
    'Country Code': 'code',
    'Value': 'population',
})
df.head(2)
country code population time
0 Aruba ABW 54922.0 1960-01-01
1 Aruba ABW 55578.0 1961-01-01
# on choisit un index un peu plus parlant

df = df.set_index('time')
df.head(2)
country code population
time
1960-01-01 Aruba ABW 54922.0
1961-01-01 Aruba ABW 55578.0
# on se définit arbitrairement une période qui nous intéresse

begin = pd.to_datetime('1980', format='%Y')
end = pd.to_datetime('2010', format='%Y')
# on filtre ce qui nous intéresse

df_france = df[(df.country == 'France') & (df.index >= begin) & (df.index <= end)]
df_france.head
<bound method NDFrame.head of            country code  population
time                               
1980-01-01  France  FRA  55274184.0
1981-01-01  France  FRA  55603353.0
1982-01-01  France  FRA  55806789.0
1983-01-01  France  FRA  56108330.0
1984-01-01  France  FRA  56383085.0
1985-01-01  France  FRA  56665619.0
1986-01-01  France  FRA  56956002.0
1987-01-01  France  FRA  57266167.0
1988-01-01  France  FRA  57598186.0
1989-01-01  France  FRA  57943062.0
1990-01-01  France  FRA  58261012.0
1991-01-01  France  FRA  58554242.0
1992-01-01  France  FRA  58846584.0
1993-01-01  France  FRA  59103094.0
1994-01-01  France  FRA  59324863.0
1995-01-01  France  FRA  59541294.0
1996-01-01  France  FRA  59754504.0
1997-01-01  France  FRA  59968066.0
1998-01-01  France  FRA  60190684.0
1999-01-01  France  FRA  60501986.0
2000-01-01  France  FRA  60918661.0
2001-01-01  France  FRA  61364377.0
2002-01-01  France  FRA  61812142.0
2003-01-01  France  FRA  62249855.0
2004-01-01  France  FRA  62707588.0
2005-01-01  France  FRA  63180854.0
2006-01-01  France  FRA  63622342.0
2007-01-01  France  FRA  64016890.0
2008-01-01  France  FRA  64375116.0
2009-01-01  France  FRA  64706436.0
2010-01-01  France  FRA  65026211.0>
# on peut maintenant dessiner

df_france[['population']].plot();
../../_images/4b9389c7089c99918101b3d87eaa3a0242f777a772ffa1aeffb5436e749a68ee.png