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
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 !)
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
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
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')
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 :
-
URLErrorpour un problème de connectivité -
HTTPErrorquand 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é !")
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
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.
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
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.")
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.txtune sectionUser-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()renvoieFalse. Si le chemin est présent dans une directiveAllow(autorisé),can_fetch()renvoieTrue -
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/
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 *.