2. Internet

Python est un langage universel, permettant une utilisation simple d’opérations complexes en les encapsulant dans des modules de haut niveau. Ce chapître traite de l’accès à des ressources distantes, et plus particulièrement de l’accès à des ressources disponibles sur le web.

2.1. Effectuer une requête

Le module standard urllib.request comporte un grand nombre de fonctions et de classes permettant de traiter les requêtes HTTP et HTTPS. Nous allons nous concentrer ici sur l’essentiel, la récupération des données contenues sur une page web.

La fonction urlopen() permet d’ouvrir une URL distante.

>>> import urllib.request
>>> u = urllib.request.urlopen('https://www.esiee.fr/')

L’objet retourné est de type HTTPResponse.

>>> type(u)
<class 'http.client.HTTPResponse'>

Cet objet est de type file-like et dispose, directement ou par héritage de la classe IOBase, de différentes méthodes de lecture similaires à celles déjà vues pour les fichiers:

Le code source de la page web peut être lu et stocké dans une liste de bytes avec readlines() :

>>> data = u.readlines()
>>> len(data)
1087

Au moment de la rédaction de ce document, la première ligne du fichier HTML du site web d”ESIEE Paris (Ctrl-u pour obtenir le code source de la page) :

>>> data[0]
b'<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->\r\n'

L’affichage est préfixé par b caractéristique des objets de type bytes.

>>> type(data[0])
<class 'bytes'>

2.2. Encodage - décodage

Comme on vient de le voir, les méthodes de lecture retournent des bytes qui doivent être convertis en str.

La conversion nécessite une table d’encodage-décodage. On travaillera essentiellement avec utf8.

L’encodage consiste à transformer des str en bytes:

>>> 'é'.encode('utf8') # utf8 code
b'\xc3\xa9'

Si le caractère n’a pas de représentation dans le charset considéré une erreur est produite:

>>> "é".encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 0: ordinal not in range(128)

Le décodage consiste à transformer des bytes en str.

>>> b'\xc3\xa9'.decode('utf8')
'é'

Important

Une mauvaise représentation des caractères accentués est souvent un problème de charset différent utilisé pour les opérations d’encodage puis de décodage.

>>> e = "éphémère".encode('utf8')
>>> e
b'\xc3\xa9ph\xc3\xa9m\xc3\xa8re'
>>> e.decode('latin1')
'éphémère'
>>> e.decode('utf8')
'éphémère'
>>>

L’information d’encodage n’est pas systématiquement contenue dans la ressource elle même. C’est notamment le cas pour les fichiers. Mais les pages Internet comportent généralement une indication sur le charset utilisé. Pour le site d”ESIEE Paris, le code source de la page contient cette information :

...
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
...

Pour illustrer cette opération de décodage, prenons l’exemple de la balise <title>:

for line in data:
    if '<title>' in line.decode('utf8'):
        title = line
        break

L’affichage sous forme de bytes utilise le code UTF8 des caractères.

>>> title
b'  <title>ESIEE Paris, l\xe2\x80\x99\xc3\xa9cole de l\xe2\x80\x99innovation technologique | Grande \xc3\xa9cole d\xe2\x80\x99ing\xc3\xa9nieurs | ESIEE Paris</title>\r\n'

L’opération de décodage permet de les remplacer par des caractères imprimables:

>>> title.decode('utf8')
'  <title>ESIEE Paris, l’école de l’innovation technologique | Grande école d’ingénieurs | ESIEE Paris</title>\r\n'
>>>

2.3. Parser une page HTML

Les informations contenues dans une page HTML sont contenues dans des balises structurées qu’il est possible d’explorer automatiquement avec un parser.

Pour cette opération, le module standard parser propose la classe HTMLParser. Cette classe est une classe générique qui propose des méthodes se déclenchant sur des évènements rencontrés lors du parcours de la page HTML. En particulier:

  • handle_starttag() se déclenche lorsque le parser rencontre une balise ouvrante ;

  • handle_endtag() se déclenche lorsque le parser rencontre une balise fermante ;

  • handle_data() se déclenche lorsque le parser se trouve entre une balise ouvrante et une balise fermante.

Bien sûr, il faut contextualiser le comportement de ces méthodes en fonction du problème à traiter. La solution est de mettre en oeuvre le principe d’héritage en sous classant HTMLParser et en redéfinissant les méthodes précédentes conformément au cahier des charges. Dans l’exemple ci dessous, les méthodes ont juste été redéfinies pour afficher à l’écran la structure de la page web:

import urllib.request
from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):

    def __init__(self):
        super().__init__()
    def handle_starttag(self, tag, attrs):
        print("<< Trouvé balise ouvrante :", tag)
    def handle_endtag(self, tag):
        print(">> Trouvé balise fermante :", tag)
    def handle_data(self, data):
        if data.strip(): print("    Trouvé contenu  :", data)

Pour utiliser cette classe, on crée une instance:

parser = MyHTMLParser()

On récupère des données à parser sous la forme d’une seule chaine de caractères:

u = urllib.request.urlopen('https://httpbin.org/html')
data = u.read().decode('utf8')

Et on alimente le parser avec ces données:

parser.feed(data)

L’affichage (tronqué) correspondant:

<< Trouvé balise ouvrante : html
<< Trouvé balise ouvrante : head
>> Trouvé balise fermante : head
<< Trouvé balise ouvrante : body
<< Trouvé balise ouvrante : h1
    Trouvé contenu  : Herman Melville - Moby-Dick
>> Trouvé balise fermante : h1
<< Trouvé balise ouvrante : div
<< Trouvé balise ouvrante : p
    Trouvé contenu  :
          Availing himself of the mild, ... and all of them a care-killing competency.

>> Trouvé balise fermante : p
>> Trouvé balise fermante : div
>> Trouvé balise fermante : body
>> Trouvé balise fermante : html

2.4. Le format JSON

JSON est un format permettant de représenter de l’information structurée comme XML mais présente l’avantage par rapport à celui ci d’être beaucoup moins verbeux. C’est devenu un standard de fait dans l’échange de données sur Internet.

Python dispose du module standard json permettant de convertir les structures de données Python au format JSON et vice versa.

La fonction dumps() retourne une chaîne de caractère contenant l’objet JSON correspondant à la structure de données Python passée en argument. Les deux formats sont très proches.

Créons une liste Python:

>>> import string
>>> l = [ c for c in string.digits]
>>> l
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

Sa représentation est quasi identique:

>>> json.dumps(l)
'["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]'

Idem pour un dictionnaire:

>>> d = { c:ord(c) for c in string.digits}
>>> d
{'6': 54, '0': 48, '2': 50, '8': 56, '7': 55, '1': 49, '4': 52, '5': 53, '9': 57, '3': 51}

dont la représentation est très proche à un délimiteur de chaine de caractères près:

>>> json.dumps(d)
'{"6": 54, "0": 48, "2": 50, "8": 56, "7": 55, "1": 49, "4": 52, "5": 53, "9": 57, "3": 51}'

La fonction loads() réalise l’opération inverse en transformant un objet JSON en une structure de données Python:

>>> d = json.loads('{"results" : [{"geometry" : {"location" : {"lat" : 48.8398094,"lng" : 2.5840685}}}]}')
>>> type(d)
<class 'dict'>

dump() et load() réalisent la même opération avec des fichiers plutôt qu’avec des chaînes de caractères.

Astuce

Le travail avec les fichiers json est facilité dans Visual Studio Code par l’extension Prettify JSON. Elle s’installe à partir du menu Affichage > Extensions.

2.5. Exercice

IMDb recense un grand nombre d’informations sur les films et les séries. En particulier, la liste des 250 meilleurs films selon les votes de ses utilisateurs. Cette liste évolue au fil du temps mais le résultat devrait être proche de ceci:

  • 1 : Les évadés

  • 2 : Le parrain

  • 3 : The Dark Knight: Le chevalier noir

  • 4 : Le parrain, 2ème partie

  • 5 : 12 hommes en colère

  • 246 : Aladdin

  • 247 : La couleur des sentiments

  • 248 : La Belle et la Bête

  • 249 : Du rififi chez les hommes

  • 250 : Danse avec les loups

L’objectif est d’écrire un parser qui récupère dynamiquement les données de cette page et affiche la liste du film le moins populaire vers le film le plus populaire.

Pour cet exercice, vous devez utiliser en priorité le fichier squelette ex01_internet.py.

2.5.1. La fonction principale

Ecrire une fonction main() :

  • qui ne prend aucun argument ;

  • et ne retourne rien.

Cependant cette fonction devra :

  • utiliser le module urllib.request pour récupérer automatiquement le contenu de la page IMDb sous la forme d’une seule chaine de caractères ;

  • mettre en oeuvre la gestion d’exception pour l’accès à la ressource distante ;

  • et faire un appel à une fonction scrap_imdb() décrite ci dessous.

Indication

La plupart des serveurs rejettent les requêtes lorsqu’elles n’émanent pas d’un navigateur. Pour éviter ce problème, il faut ajouter un en-tête HTTP User-Agent à la requête. La valeur de cet en-tête doit être une chaine de caractères qui identifie le navigateur. On trouvera les User-Agent les plus courants dans cette liste.

La requête se construit alors comme suit:

import urllib.request

firefox = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0'

req = urllib.request.Request('http://www.example.com/')
# Customize the default User-Agent header value:
req.add_header('User-Agent', firefox)
r = urllib.request.urlopen(req)

2.5.2. Scraping

Ecrire la fonction scrap_imdb() :

  • prend en argument le contenu de la page IMDb ;

  • et retourne la liste des films.

Elle devra instancier une classe IMDBParser (décrite ci dessous) et appeler la méthode feed() sur cette instance.

2.5.3. Le parser

La classe IMDBParser :

  • devra hériter de HTMLParser ;

  • utiliser deux attributs : un conteneur pour le stockage des films, l’autre pour contrôler la logique du processus ;

  • et en fonction de la structure du document HTML parsé :
  • redéfinir la méthode handle_data().

Cette logique devra déclencher l’ajout de données à un conteneur seulement sur certaines conditions. Le conteneur et les conditions seront des attributs de la classe IMDBParser.

Une fois la fonction main() opérationnelle, lancer les doctests. Ceux ci fonctionnent avec un fichier statique IMDB.html qui doit être présent dans le répertoire.