Le module urllib

Apprenez à utiliser le module urllib de Python.

Publié le par Gabriel Trouvé (mis à jour le )

30 minutes

En tant que développeur, le besoin d'interagir avec le web est omniprésent : récupérer des données, effectuer des requêtes API, automatiser des téléchargements, etc.

Vous avez certainement entendu parler de requests ou httpx, mais nous n'avons pas forcément envie d'alourdir nos dépendances.

Bon, il est maintenant temps d'introduire urllib de la bibliothèque standard de Python. Ce module est découpé en 4 sous-modules complémentaires : request, error, parse et robotparser.

Effectuer des requêtes avec urllib.request

Les bases

Pour lire le contenu d'une page web ou interroger une API, vous aurez besoin de la fonction urlopen() :

import urllib.request
import json

# On interroge une API publique (JSONPlaceholder) sans restriction
url = "https://jsonplaceholder.typicode.com/users/1"
reponse = urllib.request.urlopen(url)

# On lit le contenu et on le décode (les données arrivent en octets)
donnees_brutes = reponse.read().decode('utf-8')

# On convertit la chaîne JSON en dictionnaire Python
utilisateur = json.loads(donnees_brutes)

print(f"Nom : {utilisateur['name']}") # Affiche : Nom : Leanne Graham
print(f"Ville : {utilisateur['address']['city']}") # Affiche : Ville : Gwenborough
PYTHON

JSONPlaceholder est une fausse API gratuite, parfaite pour faire des tests. Elle fournit des données aléatoires réalistes au format JSON. Dans cet exemple, nous récupérons des informations sur un utilisateur. Il faut convertir le JSON en dictionnaire Python via la bibliothèque json.

Personnaliser la requête avec l'objet Request

Bien souvent, un simple appel avec urlopen() ne suffit pas. Beaucoup de serveurs bloquent les requêtes qui n'ont pas d'en-tête valide, renvoyant une erreur 403.

Nous allons simuler un navigateur en passant le User-Agent :

import urllib.request

# Wikipedia bloque les requêtes sans User-Agent personnalisé (Erreur 403)
url = "https://fr.wikipedia.org/wiki/Python_(langage)"

# Création d'un dictionnaire avec nos en-têtes personnalisés
# Ici nous utilisons un User-Agent générique pour simuler un navigateur web classique
entetes = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
}

# On crée l'objet Request (au lieu de passer juste la chaîne de l'URL)
ma_requete = urllib.request.Request(url, headers=entetes)

# On passe l'objet Request à urlopen
reponse = urllib.request.urlopen(ma_requete)
print(reponse.status) # Affiche : 200 (Sans l'en-tête, cela aurait planté avec une erreur 403 !)
PYTHON

Et c'est d'ailleurs de cette manière que l'on passe nos jetons d'authentification (tokens d'API) :

import urllib.request

# Postman Echo est un excellent service de test
url = "https://postman-echo.com/get"
jeton_api = "le_super_token_secret_de_patrick"

# Ajout du token d'authentification ET d'un User-Agent dans les en-têtes
entetes = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
    'Authorization': f'Bearer {jeton_api}' 
}

ma_requete = urllib.request.Request(url, headers=entetes)
reponse = urllib.request.urlopen(ma_requete)

print(reponse.status) # Affiche : 200
PYTHON

Dans cet exemple, "le_super_token_secret_de_patrick" est un jeton factice suffisant pour la démonstration.

Envoyer des données (Requête POST)

Il arrive que l'on doive envoyer des données à un serveur, que ce soit pour soumettre un formulaire ou transmettre un payload JSON à une API. Il faut alors utiliser une requête POST en passant un argument data encodé en octets à la fonction urlopen().

import urllib.request
import json

# Utilisation de Postman Echo pour tester le POST
url = "https://postman-echo.com/post"
payload = {"utilisateur": "Patrick", "role": "admin"}

# On convertit le dictionnaire en JSON, puis on l'encode en octets
donnees_encodees = json.dumps(payload).encode()

# On précise que l'on envoie du JSON via les en-têtes
entetes = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
    'Content-Type': 'application/json'
}

requete = urllib.request.Request(url, data=donnees_encodees, headers=entetes)
reponse = urllib.request.urlopen(requete)

print(reponse.status) # Affiche : 200
PYTHON

Opener et Handlers

Pour des besoins spécifiques, urllib.request nous permet d'utiliser des Handlers. Il s'agit de gestionnaires capables de s'occuper de tâches plus complexes qui seront exécutées par un Opener.

Par exemple, si vous êtes derrière un proxy d'entreprise, vous ne pourrez pas utiliser un simple urlopen(). Il faudra utiliser un ProxyHandler.

import urllib.request

# Configuration du proxy
proxy_handler = urllib.request.ProxyHandler({'http': 'http://monproxy.com:8080/'})

# Création d'un "Opener" personnalisé avec ce handler
opener = urllib.request.build_opener(proxy_handler)

reponse = opener.open('http://www.python.org')
PYTHON

Attention

Il est également possible d'installer un opener de manière globale via urllib.request.install_opener(opener). Tous les futurs appels à la fonction urlopen() passeront par le proxy pour toute la durée du programme. C'est un effet de bord risqué, c'est pourquoi l'on préfère une exécution isolée avec opener.open().

À noter

Il existe des Handlers pour différentes utilisations : HTTPBasicAuthHandler pour s'authentifier par mot de passe, HTTPCookieProcessor pour gérer les sessions, etc.

Gérer les erreurs avec urllib.error

Son nom est parlant : le sous-module urllib.error permet de gérer les caprices potentiels du réseau, tels que les coupures de connexion, une API qui ne répond plus ou un problème de permission.

Il y a deux exceptions principales à connaître :

  • URLError pour un problème de connectivité

  • HTTPError quand le serveur répond avec une erreur HTTP

Attention

HTTPError hérite de URLError. L'ordre de vos blocs except est donc important : il faut toujours capturer HTTPError en premier.

import urllib.request
import urllib.error

# On génère volontairement une erreur 404 avec Postman Echo
url = "https://postman-echo.com/status/404"

try:
    reponse = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
    # Le serveur a répondu, mais avec un code d'erreur
    print(f"Erreur du serveur : {e.code}") # Affiche : Erreur du serveur : 404
    # Astuce : lire le corps de l'erreur pour comprendre le problème
    print(f"Détails : {e.read().decode()}")

# Amusez-vous à couper le wifi pour tester l'exception suivante ahah
except urllib.error.URLError as e:
    # On n'a même pas pu joindre le serveur
    print(f"Impossible de joindre le serveur : {e.reason}")
else:
    print("Tout s'est bien passé !")
PYTHON

Décortiquer et construire des URLs avec urllib.parse

Anatomie d'une URL

Parfois, vous aurez besoin de décomposer une URL (qu'elle soit simple ou complexe) en ses différents composants : schéma, domaine, chemin, paramètres. Pour cela, on utilisera urlparse.

from urllib.parse import urlparse

url = "https://www.docstring.fr/formations/quizzes/?query=tosa"
analyse = urlparse(url)

print(analyse.scheme) # Affiche : https
print(analyse.netloc) # Affiche : www.docstring.fr
print(analyse.path)   # Affiche : /formations/quizzes/
print(analyse.query)  # Affiche : query=tosa
PYTHON

Gérer les paramètres

La fonction urlencode() permet de transformer un dictionnaire Python en une chaîne de requête. Cela évite les concaténations de chaînes hasardeuses avec des ? et &. De plus, elle s'occupe automatiquement d'encoder les caractères spéciaux pour vous.

from urllib.parse import urlencode

parametres = {
    "search": "python & django",
    "auteur": "Patrick Åström",
    "difficulty": 2
}

chaine_requete = urlencode(parametres)
print(chaine_requete) 
# Affiche : search=python+%26+django&auteur=Patrick+%C3%85str%C3%B6m&difficulty=2

# Utilisation d'un service de test pour éviter une erreur 404 si on teste le lien !
url_complete = f"https://postman-echo.com/get?{chaine_requete}"
print(url_complete)
# https://postman-echo.com/get?search=python+%26+django&auteur=Patrick+%C3%85str%C3%B6m&difficulty=2
# Vous pouvez copier-coller ce lien dans votre navigateur pour voir les paramètres correctement encodés.
PYTHON

L'encodage des caractères spéciaux

Bien que urlencode() soit conçu pour traiter les dictionnaires et construire les paramètres de requête, il utilise quote() sous le capot.
Il est donc possible d'utiliser quote() de manière isolée pour sécuriser une simple chaîne de caractères.

from urllib.parse import quote, unquote

nom_dossier = "Pätrîck et programmation"

# On sécurise un texte isolé pour qu'il soit "URL-safe"
texte_securise = quote(nom_dossier)

print(texte_securise) # Affiche : P%C3%A4tr%C3%AEck%20et%20programmation
print(f"https://mon-site.com/dossiers/{texte_securise}")

# Et pour faire l'inverse
print(unquote(texte_securise)) # Affiche : Pätrîck et programmation
PYTHON

Adopter la bonne conduite avec urllib.robotparser

Nous n'allions pas parler d'automatisation web sans évoquer l'éthique, voyons ! Avant de récupérer les données d'un site, il faut s'assurer d'en avoir le droit.

Bien souvent, les sites fournissent un fichier robots.txt à la racine de leur domaine. Ce fichier indique quelles pages peuvent être parcourues et par qui. urllib.robotparser permet d'interpréter ce fichier.

import urllib.robotparser

# On instancie l'analyseur
rp = urllib.robotparser.RobotFileParser()

# On lui indique où trouver le fichier robots.txt
rp.set_url("https://www.python.org/robots.txt")

# On lit et on analyse le fichier
rp.read()
print(rp)

# Notre script (par exemple un bot "PatrickBot") a-t-il le droit
# de lire le chemin "/downloads/" ?
droit_acces = rp.can_fetch("PatrickBot", "https://www.python.org/downloads/")

if droit_acces:
    print("Super, je peux récupérer cette page !")
else:
    print("Dommage, l'administrateur a interdit l'accès à ce chemin.")
PYTHON

Comment Python sait-il si l'accès est autorisé ?

La méthode can_fetch() n'a rien de magique. Elle renvoie True ou False selon certaines règles :

  • Qui êtes-vous ? Python cherche dans le robots.txt une section User-agent: qui correspond au nom de votre bot. S'il ne trouve rien, il se rabat sur la section universelle qui s'applique à tout le monde : User-agent: *

  • Qu'avez-vous le droit de faire ? Si votre chemin commence par un dossier présent dans une directive Disallow (interdit), can_fetch() renvoie False. Si le chemin est présent dans une directive Allow (autorisé), can_fetch() renvoie True

  • Si le chemin que vous voulez visiter n'est pas mentionné, alors il est autorisé par défaut

Voici l'extrait du robots.txt de python.org :

User-agent: HTTrack
User-agent: puf
User-agent: MSIECrawler
Disallow: /

User-agent: Krugle
Allow: /
Disallow: /~guido/orlijn/
Disallow: /webstats/

User-agent: Nutch
Disallow: /

User-agent: *
Disallow: /~guido/orlijn/
Disallow: /webstats/
SHELL

Krugle et Nutch sont de vrais robots, et chez python.org, des règles leur ont été imposées. Par exemple, on voit bien que tout est interdit à Nutch, tandis que Krugle ne pourra pas visiter /webstats/ et /~guido/orlijn/.
Pour un robot que l'on appellera PatrickBot, comme il n'est pas mentionné, Python le placera dans le groupe universel *.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.