Le webscraping désigne les techniques d’extraction du contenu des sites internet. C’est une pratique très utile pour toute personne souhaitant travailler sur des informations disponibles en ligne, mais n’existant pas forcément sous la forme d’un tableau Excel.
Ce TP vous présente comment créer et exécuter des robots afin de recupérer rapidement des informations utiles à vos projets actuels ou futurs. Il part de quelques cas d’usages concret. Ce chapitre est très fortement inspiré et réadapté à partir de celui de Xavier Dupré, l’ancien professeur de la matière.
Enjeux
Un certain nombre d’enjeux du webscraping ne seront évoqués que superficiellement dans le cadre de ce chapitre.
La zone grise de la légalité du webscraping
En premier lieu, en ce qui concerne la question de la légalité de la récupération d’information par scraping, il existe une zone grise. Ce n’est pas parce qu’une information est disponible sur internet, directement ou avec un peu de recherche, qu’elle peut être récupérée et réutilisée.
L’excellent cours d’Antoine Palazzolo évoque un certain nombre de cas médiatiques et judiciaires sur cette question. Dans le champ français, la CNIL a publié en 2020 de nouvelles directives sur le webscraping reprécisant que toute donnée ne peut être réutilisée à l’insu de la personne à laquelle ces données appartiennent. Autrement dit, en principe, les données collectées par webscraping sont soumises au RGPD, c’est-à-dire nécessitent le consentement des personnes à partir desquelles la réutilisation des données est faite.
Il est donc recommandé d’être vigilant avec les données récupérées par webscraping pour ne pas se mettre en faute légalement.
Stabilité et fiabilité des informations reçues
La récupération de données par webscraping est certes pratique mais elle ne correspond pas nécessairement à un usage pensé, ou désiré, par un fournisseur de données. Les données étant coûteuses à collecter et à mettre à disposition, certains sites ne désirent pas nécessairement que celles-ci soient extraites gratuitement et facilement. A fortiori lorsque la donnée peut permettre à un concurrent de disposer d’une information utile d’un point de vue commercial (prix d’un produit concurrent, etc.).
Les acteurs mettent donc souvent en oeuvre des stratégies pour bloquer ou limiter la quantité de données scrappées. La méthode la plus classique est la détection et le blocage des requêtes faites par des robots plutôt que par des humains. Pour des acteurs spécialisés, cette détection est très facile car de nombreuses preuves permettent d’identifier si une visite du site web provient d’un utilisateur humain derrière un navigateur ou d’un robot. Pour ne citer que quelques indices: vitesse de la navigation entre pages, rapidité à extraire la donnée, empreinte digitale du navigateur utilisé, capacité à répondre à des questions aléatoires (captcha)… Les bonnes pratiques, évoquées par la suite, ont pour objectif de faire en sorte qu’un robot se comporte de manière civile en adoptant un comportement proche de celui de l’humain mais sans contrefaire le fait qu’il ne s’agit pas d’un humain.
Il convient d’ailleurs d’être prudent quant aux informations reçues par webscraping. La donnée étant au coeur du modèle économique de certains acteurs, certains n’hésitent pas à renvoyer des données fausses aux robots plutôt que les bloquer. C’est de bonne guerre! Une autre technique piège s’appelle le honey pot. Il s’agit de pages qu’un humain n’irait jamais visiter - par exemple parce qu’elles n’apparaissent pas dans l’interface graphique - mais sur lesquelles un robot, en recherche automatique de contenu, va rester bloquer.
Sans aller jusqu’à la stratégie de blocage du webscraping, d’autres raisons peuvent expliquer qu’une récupération de données ait fonctionné par le passé mais ne fonctionne plus. La plus fréquente est un changement dans la structure d’un site web. Le webscraping présente en effet l’inconvénient d’aller chercher de l’information dans une structure très hiérarchisée. Un changement dans cette structure peut suffire à rendre un robot incapable de récupérer du contenu. Or, pour rester attractifs, les sites web changent fréquemment ce qui peut facilement rendre inopérant un robot.
De manière générale, l’un des principaux messages de ce chapitre, à retenir, est que le webscraping est une solution de dernier ressort, pour des récupérations ponctuelles de données sans garantie de fonctionnement ultérieur. Il est préférable de privilégier les API lorsque celles-ci sont disponibles. Ces dernières ressemblent à un contrat (formel ou non) entre un fournisseur de données et un utilisateur où sont définis des besoins (les données) mais aussi des conditions d’accès (nombre de requêtes, volumétrie, authentification…) là où le webscraping est plus proche du comportement dans le Far West.
Les bonnes pratiques
La possibilité de récupérer des données par l’intermédiaire d’un robot ne signifie pas qu’on peut se permettre de n’être pas civilisé. En effet, lorsqu’il est non-maîtrisé, le webscraping peut ressembler à une attaque informatique classique pour faire sauter un site web: le déni de service. Le cours d’Antoine Palazzolo revient sur certaines bonnes pratiques qui ont émergé dans la communauté des scrapeurs. Il est recommandé de lire cette ressource pour en apprendre plus sur ce sujet. Y sont évoqués plusieurs conventions, parmi lesquelles :
- Se rendre, depuis la racine du site,
sur le fichier
robots.txt
pour vérifier les consignes proposées par les développeurs du site web pour cadrer le comportement des robots ; - Espacer chaque requêtes de plusieurs secondes, comme le ferait un humain, afin d’éviter de surcharger le site web et de le faire sauter par déni de service ;
- Faire les requêtes dans les heures creuses de fréquentation du
site web s’il ne s’agit pas d’un site consulté internationalement.
Par exemple, pour un site en Français, lancer le robot
pendant la nuit en France métropolitaine, est une bonne pratique.
Pour lancer un robot depuis
Python
a une heure programmée à l’avancer, il existe lescronjobs
.
Un détour par le Web : comment fonctionne un site ?
Même si ce TP ne vise pas à faire un cours de web, il vous faut néanmoins certaines bases sur la manière dont un site internet fonctionne afin de comprendre comment sont structurées les informations sur une page.
Un site Web est un ensemble de pages codées en HTML qui permet de décrire à la fois le contenu et la forme d’une page Web.
Pour voir cela, ouvrez n’importe quelle page web et faites un clic-droit dessus.
- Sous
Chrome
: Cliquez ensuite sur “Affichez le code source de la page” (CTRL+U) ; - Sous
Firefox
: “Code source de la page” (CTRL+MAJ+K) ; - Sous
Edge
: “Affichez la page source” (CTRL+U) ; - Sous
Safari
: voir comment faire ici
Les balises
Sur une page web, vous trouverez toujours à coup sûr des éléments comme <head>
, <title>
, etc. Il s’agit des codes qui vous permettent de structurer le contenu d’une page HTML et qui s’appellent des balises.
Citons, par exemple, les balises <p>
, <h1>
, <h2>
, <h3>
, <strong>
ou <em>
.
Le symbole < >
est une balise : il sert à indiquer le début d’une partie. Le symbole </ >
indique la fin de cette partie. La plupart des balises vont par paires, avec une balise ouvrante et une balise fermante (par exemple <p>
et </p>
).
Exemple : les balise des tableaux
Balise | Description |
---|---|
<table> |
Tableau |
<caption> |
Titre du tableau |
<tr> |
Ligne de tableau |
<th> |
Cellule d’en-tête |
<td> |
Cellule |
<thead> |
Section de l’en-tête du tableau |
<tbody> |
Section du corps du tableau |
<tfoot> |
Section du pied du tableau |
Application : un tableau en HTML
Le code HTML du tableau suivant
<table>
<caption> Le Titre de mon tableau </caption>
<tr>
<th>Prénom</th>
<th>Nom</th>
<th>Profession</th>
</tr>
<tr>
<td>Mike </td>
<td>Stuntman</td>
<td>Cascadeur</td>
</tr>
<tr>
<td>Mister</td>
<td>Pink</td>
<td>Gangster</td>
</tr>
</table>
Donnera dans le navigateur :
Prénom | Nom | Profession |
---|---|---|
Mike | Stuntman | Cascadeur |
Mister | Pink | Gangster |
Parent et enfant
Dans le cadre du langage HTML, les termes de parent (parent) et enfant (child) servent à désigner des élements emboîtés les uns dans les autres. Dans la construction suivante, par exemple :
< div>
< p>
bla,bla
< /p>
< /div>
Sur la page web, cela apparaitra de la manière suivante :
<p>
bla,bla
</p>
On dira que l’élément <div>
est le parent de l’élément <p>
tandis que l’élément <p>
est l’enfant de l’élément <div>
.
Mais pourquoi apprendre ça pour “scraper” ?
Parce que, pour bien récupérer les informations d’un site internet, il faut pouvoir comprendre sa structure et donc son code HTML. Les fonctions Python
qui servent au scraping sont principalement construites pour vous permettre de naviguer entre les balises.
Avec Python
, vous allez en fait reproduire votre comportement manuel de recherche de manière
à l’automatiser.
Scraper avec python: le package BeautifulSoup
Les packages disponibles
Dans la première partie de ce chapitre,
nous allons essentiellement utiliser le package BeautifulSoup4
,
en conjonction avec urllib
ou requests
. Ces deux derniers packages permettent de récupérer le texte
brut d’une page qui sera ensuite
inspecté via BeautifulSoup4
.
BeautifulSoup
sera suffisant quand vous voudrez travailler sur des pages HTML statiques. Dès que les informations que vous recherchez sont générées via l’exécution de scripts Javascript, il vous faudra passer par des outils comme Selenium.
De même, si vous ne connaissez pas l’URL, il faudra passer par un framework comme Scrapy, qui passe facilement d’une page à une autre. On appelle
cette technique le “webcrawling”. Scrapy
est plus complexe à manipuler que BeautifulSoup
: si vous voulez plus de détails, rendez-vous sur la page du tutoriel Scrapy
.
Le webscraping est un domaine où la reproductibilité est compliquée à mettre en oeuvre. Une page web évolue potentiellement régulièrement et d’une page web à l’autre, la structure peut être très différente ce qui rend certains codes difficilement exportables. Par conséquent, la meilleure manière d’avoir un programme fonctionnel est de comprendre la structure d’une page web et dissocier les éléments exportables à d’autres cas d’usages des requêtes ad hoc.
My experience of web scraping pic.twitter.com/8Dsz7amDkV
— Lino Galiana (@LinoGaliana) December 24, 2021
import urllib
import bs4
import pandas
from urllib import request
Récupérer le contenu d’une page HTML
On va commencer doucement. Prenons une page wikipedia, par exemple celle de la Ligue 1 de football, millésime 2019-2020 : Championnat de France de football 2019-2020. On va souhaiter récupérer la liste des équipes, ainsi que les url des pages Wikipedia de ces équipes.
Etape 1️⃣ : se connecter à la page wikipedia et obtenir le code source.
Pour cela, le plus simple est d’utiliser le package urllib
ou, mieux, requests
.
Nous allons ici utiliser la fonction request
du package urllib
:
url_ligue_1 = "https://fr.wikipedia.org/wiki/Championnat_de_France_de_football_2019-2020"
request_text = request.urlopen(url_ligue_1).read()
# print(request_text[:1000])
type(request_text)
bytes
Etape 2️⃣ : utiliser le package BeautifulSoup
qui permet de rechercher efficacement
les balises contenues dans la chaine de caractères
renvoyée par la fonction request
:
page = bs4.BeautifulSoup(request_text, "lxml")
Si on print l’objet page
créée avec BeautifulSoup
,
on voit que ce n’est plus une chaine de caractères mais bien une page HTML avec des balises.
On peut à présent chercher des élements à l’intérieur de ces balises.
La méthode find
Par exemple, si on veut connaître le titre de la page, on utilise la méthode .find
et on lui demande “title”
print(page.find("title"))
<title>Championnat de France de football 2019-2020 — Wikipédia</title>
La methode .find
ne renvoie que la première occurence de l’élément.
Pour vous en assurer vous pouvez :
- copier le bout de code source obtenu,
- le coller dans une cellule de votre notebook
- et passer la cellule en “Markdown”
La cellule avec le copier-coller du code source donne :
print(page.find("table"))
<table><caption style="background-color:#99cc99;color:#000000;">Généralités</caption><tbody><tr>
<th scope="row" style="width:10.5em;">Sport</th>
<td>
<a href="/wiki/Football" title="Football">Football</a></td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Organisateur(s)</th>
<td>
<a href="/wiki/Ligue_de_football_professionnel" title="Ligue de football professionnel">LFP</a></td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Édition</th>
<td>
<abbr class="abbr" title="Quatre-vingt-deuxième (huitante-deuxième / octante-deuxième)">82<sup>e</sup></abbr></td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Lieu(x)</th>
<td>
<span class="datasortkey" data-sort-value="France"><span class="flagicon"><a class="image" href="/wiki/Fichier:Flag_of_France.svg" title="Drapeau de la France"><img alt="Drapeau de la France" class="noviewer thumbborder" data-file-height="600" data-file-width="900" decoding="async" height="13" src="//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Flag_of_France.svg/20px-Flag_of_France.svg.png" srcset="//upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Flag_of_France.svg/30px-Flag_of_France.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Flag_of_France.svg/40px-Flag_of_France.svg.png 2x" width="20"/></a> </span><a href="/wiki/France" title="France">France</a></span> et <br/><span class="datasortkey" data-sort-value="Monaco"><span class="flagicon"><a class="image" href="/wiki/Fichier:Flag_of_Monaco.svg" title="Drapeau de Monaco"><img alt="Drapeau de Monaco" class="noviewer thumbborder" data-file-height="800" data-file-width="1000" decoding="async" height="16" src="//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Flag_of_Monaco.svg/20px-Flag_of_Monaco.svg.png" srcset="//upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Flag_of_Monaco.svg/30px-Flag_of_Monaco.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Flag_of_Monaco.svg/40px-Flag_of_Monaco.svg.png 2x" width="20"/></a> </span><a href="/wiki/Monaco" title="Monaco">Monaco</a></span></td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Date</th>
<td>
Du <time class="nowrap date-lien" data-sort-value="2019-08-09" datetime="2019-08-09"><a href="/wiki/9_ao%C3%BBt_en_sport" title="9 août en sport">9</a> <a class="mw-redirect" href="/wiki/Ao%C3%BBt_2019_en_sport" title="Août 2019 en sport">août</a> <a href="/wiki/2019_en_football" title="2019 en football">2019</a></time><br/>au <time class="nowrap date-lien" data-sort-value="2020-03-08" datetime="2020-03-08"><a href="/wiki/8_mars_en_sport" title="8 mars en sport">8 mars</a> <a href="/wiki/2020_en_football" title="2020 en football">2020</a></time> <small>(arrêt définitif)</small></td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Participants</th>
<td>
20 équipes</td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Matchs joués</th>
<td>
279 (sur 380 prévus)</td>
</tr>
<tr>
<th scope="row" style="width:10.5em;">Site web officiel</th>
<td>
<a class="external text" href="https://www.ligue1.fr/" rel="nofollow">Site officiel</a></td>
</tr></tbody></table>
ce qui est le texte source permettant de générer le tableau suivant:
Sport | Football |
---|---|
Organisateur(s) | LFP |
Édition | 82e |
Lieu(x) |
![]() ![]() |
Date |
Du au (arrêt définitif) |
Participants | 20 équipes |
Matchs joués | 279 (sur 380 prévus) |
Site web officiel | Site officiel |
La méthode findAll
Pour trouver toutes les occurences, on utilise .findAll()
.
print("Il y a", len(page.findAll("table")), "éléments dans la page qui sont des <table>")
Il y a 34 éléments dans la page qui sont des <table>
Exercice guidé : obtenir la liste des équipes de Ligue 1
Dans le premier paragraphe de la page “Participants”, on a le tableau avec les résultats de l’année.
Exercice 1 : Récupérer les participants de la Ligue 1
Pour cela, nous allons procéder en 6 étapes:
- Trouver le tableau
- Récupérer chaque ligne du table
- Nettoyer les sorties en ne gardant que le texte sur une ligne
- Généraliser sur toutes les lignes
- Récupérer les entêtes du tableau
- Finalisation du tableau
1️ Trouver le tableau
# on identifie le tableau en question : c'est le premier qui a cette classe "wikitable sortable"
tableau_participants = page.find('table', {'class' : 'wikitable sortable'})
print(tableau_participants)
2️ Récupérer chaque ligne du tableau.
On recherche d’abord toutes les lignes du tableau avec la balise tr
table_body = tableau_participants.find('tbody')
rows = table_body.find_all('tr')
On obtient une liste où chaque élément est une des lignes du tableau Pour illustrer cela, on va d’abord afficher la première ligne. Celle-ci correspont aux entêtes de colonne:
print(rows[0])
<tr>
<th scope="col">Club
</th>
<th scope="col">Dernière<br/>montée
</th>
<th scope="col">Budget<sup class="reference" id="cite_ref-3"><a href="#cite_note-3"><span class="cite_crochet">[</span>3<span class="cite_crochet">]</span></a></sup><br/>en M<a href="/wiki/Euro" title="Euro">€</a>
</th>
<th scope="col">Classement<br/><a href="/wiki/Championnat_de_France_de_football_2018-2019" title="Championnat de France de football 2018-2019">2018-2019</a>
</th>
<th scope="col">Entraîneur
</th>
<th scope="col">Depuis
</th>
<th scope="col">Stade
</th>
<th scope="col">Capacité<br/>en L1<sup class="reference" id="cite_ref-4"><a href="#cite_note-4"><span class="cite_crochet">[</span>4<span class="cite_crochet">]</span></a></sup>
</th>
<th scope="col">Nombre<br/>de saisons<br/>en L1
</th></tr>
La seconde ligne va correspondre à la ligne du premier club présent dans le tableau:
print(rows[1])
<tr bgcolor="#97DEFF">
<td><a href="/wiki/Paris_Saint-Germain_Football_Club" title="Paris Saint-Germain Football Club">Paris Saint-Germain</a>
</td>
<td>1974
</td>
<td>637
</td>
<td><span data-sort-value="101 !"></span><abbr class="abbr" title="Premier">1<sup>er</sup></abbr>
</td>
<td align="left"><span class="flagicon"><a class="image" href="/wiki/Fichier:Flag_of_Germany.svg" title="Drapeau : Allemagne"><img alt="" class="noviewer thumbborder" data-file-height="600" data-file-width="1000" decoding="async" height="12" src="//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/20px-Flag_of_Germany.svg.png" srcset="//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/30px-Flag_of_Germany.svg.png 1.5x, //upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/40px-Flag_of_Germany.svg.png 2x" width="20"/></a></span> <a href="/wiki/Thomas_Tuchel" title="Thomas Tuchel">Thomas Tuchel</a>
</td>
<td>2018
</td>
<td><a href="/wiki/Parc_des_Princes" title="Parc des Princes">Parc des Princes</a>
</td>
<td>47 929
</td>
<td>46
</td></tr>
3️ Nettoyer les sorties en ne gardant que le texte sur une ligne
On va utiliser l’attribut text
afin de se débarasser de toute la couche de HTML qu’on obtient à l’étape 2.
Un exemple sur la ligne du premier club :
- on commence par prendre toutes les cellules de cette ligne, avec la balise
td
. - on fait ensuite une boucle sur chacune des cellules et on ne garde que le texte de la cellule avec l’attribut
text
. - enfin, on applique la méthode
strip()
pour que le texte soit bien mis en forme (sans espace inutile etc).
cols = rows[1].find_all('td')
print(cols[0])
print(cols[0].text.strip())
<td><a href="/wiki/Paris_Saint-Germain_Football_Club" title="Paris Saint-Germain Football Club">Paris Saint-Germain</a>
</td>
Paris Saint-Germain
for ele in cols :
print(ele.text.strip())
Paris Saint-Germain
1974
637
1er
Thomas Tuchel
2018
Parc des Princes
47 929
46
4️ Généraliser sur toutes les lignes :
for row in rows:
cols = row.find_all('td')
cols = [ele.text.strip() for ele in cols]
print(cols)
[]
['Paris Saint-Germain', '1974', '637', '1er', 'Thomas Tuchel', '2018', 'Parc des Princes', '47\xa0929', '46']
['LOSC Lille', '2000', '120', '2e', 'Christophe Galtier', '2017', 'Stade Pierre-Mauroy', '49\xa0712', '59']
['Olympique lyonnais', '1989', '310', '3e', 'Rudi Garcia', '2019', 'Groupama Stadium', '57\xa0206', '60']
['AS Saint-Étienne', '2004', '100', '4e', 'Claude Puel', '2019', 'Stade Geoffroy-Guichard', '41\xa0965', '66']
['Olympique de Marseille', '1996', '110', '5e', 'André Villas-Boas', '2019', 'Orange Vélodrome', '66\xa0226', '69']
['Montpellier HSC', '2009', '40', '6e', 'Michel Der Zakarian', '2017', 'Stade de la Mosson', '22\xa0000', '27']
['OGC Nice', '2002', '50', '7e', 'Patrick Vieira', '2018', 'Allianz Riviera', '35\xa0596', '60']
['Stade de Reims', '2018', '45', '8e', 'David Guion', '2017', 'Stade Auguste-Delaune', '20\xa0546', '35']
['Nîmes Olympique', '2018', '27', '9e', 'Bernard Blaquart', '2015', 'Stade des Costières', '15\xa0788', '35']
['Stade rennais FC', '1994', '65', '10e', 'Julien Stéphan', '2018', 'Roazhon Park', '29\xa0194', '62']
['RC Strasbourg Alsace', '2017', '43', '11e', 'Thierry Laurey', '2016', 'Stade de la Meinau', '26\xa0109', '58']
['FC Nantes', '2013', '70', '12e', 'Christian Gourcuff', '2019', 'Stade de la Beaujoire - Louis Fonteneau', '35\xa0322', '51']
['SCO d’Angers', '2015', '32', '13e', 'Stéphane Moulin', '2011', 'Stade Raymond-Kopa', '14\xa0582', '27']
['Girondins de Bordeaux', '1992', '70', '14e', 'Paulo Sousa', '2019', 'Matmut Atlantique', '42\xa0115', '66']
['Amiens SC', '2017', '30', '15e', 'Luka Elsner', '2019', 'Stade Crédit Agricole la Licorne', '12\xa0999', '2']
['Toulouse FC', '2003', '35', '16e', 'Denis Zanko', '2020', 'Stadium de Toulouse', '33\xa0033', '32']
['AS Monaco', '2013', '220', '17e', 'Robert Moreno', '2019', 'Stade Louis-II', '16\xa0500', '60']
['Dijon FCO', '2016', '38', '18e', 'Stéphane Jobard', '2019', 'Parc des Sports Gaston-Gérard', '15\xa0459', '4']
['FC Metz', '2019', '40', '1er (Ligue 2)', 'Vincent Hognon', '2019', 'Stade Saint-Symphorien', '25\xa0865', '61']
['Stade brestois 29', '2019', '30', '2e (Ligue 2)', "Olivier Dall'Oglio", '2019', 'Stade Francis-Le Blé', '14\xa0920', '13']
On a bien réussi à avoir les informations contenues dans le tableau des participants du championnat. Mais la première ligne est étrange : c’est une liste vide …
Il s’agit des en-têtes : elles sont reconnues par la balise th
et non td
.
On va mettre tout le contenu dans un dictionnaire, pour le transformer ensuite en DataFrame pandas :
dico_participants = dict()
for row in rows:
cols = row.find_all('td')
cols = [ele.text.strip() for ele in cols]
if len(cols) > 0 :
dico_participants[cols[0]] = cols[1:]
dico_participants
{'Paris Saint-Germain': ['1974',
'637',
'1er',
'Thomas Tuchel',
'2018',
'Parc des Princes',
'47\xa0929',
'46'],
'LOSC Lille': ['2000',
'120',
'2e',
'Christophe Galtier',
'2017',
'Stade Pierre-Mauroy',
'49\xa0712',
'59'],
'Olympique lyonnais': ['1989',
'310',
'3e',
'Rudi Garcia',
'2019',
'Groupama Stadium',
'57\xa0206',
'60'],
'AS Saint-Étienne': ['2004',
'100',
'4e',
'Claude Puel',
'2019',
'Stade Geoffroy-Guichard',
'41\xa0965',
'66'],
'Olympique de Marseille': ['1996',
'110',
'5e',
'André Villas-Boas',
'2019',
'Orange Vélodrome',
'66\xa0226',
'69'],
'Montpellier HSC': ['2009',
'40',
'6e',
'Michel Der Zakarian',
'2017',
'Stade de la Mosson',
'22\xa0000',
'27'],
'OGC Nice': ['2002',
'50',
'7e',
'Patrick Vieira',
'2018',
'Allianz Riviera',
'35\xa0596',
'60'],
'Stade de Reims': ['2018',
'45',
'8e',
'David Guion',
'2017',
'Stade Auguste-Delaune',
'20\xa0546',
'35'],
'Nîmes Olympique': ['2018',
'27',
'9e',
'Bernard Blaquart',
'2015',
'Stade des Costières',
'15\xa0788',
'35'],
'Stade rennais FC': ['1994',
'65',
'10e',
'Julien Stéphan',
'2018',
'Roazhon Park',
'29\xa0194',
'62'],
'RC Strasbourg Alsace': ['2017',
'43',
'11e',
'Thierry Laurey',
'2016',
'Stade de la Meinau',
'26\xa0109',
'58'],
'FC Nantes': ['2013',
'70',
'12e',
'Christian Gourcuff',
'2019',
'Stade de la Beaujoire - Louis Fonteneau',
'35\xa0322',
'51'],
'SCO d’Angers': ['2015',
'32',
'13e',
'Stéphane Moulin',
'2011',
'Stade Raymond-Kopa',
'14\xa0582',
'27'],
'Girondins de Bordeaux': ['1992',
'70',
'14e',
'Paulo Sousa',
'2019',
'Matmut Atlantique',
'42\xa0115',
'66'],
'Amiens SC': ['2017',
'30',
'15e',
'Luka Elsner',
'2019',
'Stade Crédit Agricole la Licorne',
'12\xa0999',
'2'],
'Toulouse FC': ['2003',
'35',
'16e',
'Denis Zanko',
'2020',
'Stadium de Toulouse',
'33\xa0033',
'32'],
'AS Monaco': ['2013',
'220',
'17e',
'Robert Moreno',
'2019',
'Stade Louis-II',
'16\xa0500',
'60'],
'Dijon FCO': ['2016',
'38',
'18e',
'Stéphane Jobard',
'2019',
'Parc des Sports Gaston-Gérard',
'15\xa0459',
'4'],
'FC Metz': ['2019',
'40',
'1er (Ligue 2)',
'Vincent Hognon',
'2019',
'Stade Saint-Symphorien',
'25\xa0865',
'61'],
'Stade brestois 29': ['2019',
'30',
'2e (Ligue 2)',
"Olivier Dall'Oglio",
'2019',
'Stade Francis-Le Blé',
'14\xa0920',
'13']}
data_participants = pandas.DataFrame.from_dict(dico_participants,orient='index')
data_participants.head()
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
---|---|---|---|---|---|---|---|---|
Paris Saint-Germain | 1974 | 637 | 1er | Thomas Tuchel | 2018 | Parc des Princes | 47 929 | 46 |
LOSC Lille | 2000 | 120 | 2e | Christophe Galtier | 2017 | Stade Pierre-Mauroy | 49 712 | 59 |
Olympique lyonnais | 1989 | 310 | 3e | Rudi Garcia | 2019 | Groupama Stadium | 57 206 | 60 |
AS Saint-Étienne | 2004 | 100 | 4e | Claude Puel | 2019 | Stade Geoffroy-Guichard | 41 965 | 66 |
Olympique de Marseille | 1996 | 110 | 5e | André Villas-Boas | 2019 | Orange Vélodrome | 66 226 | 69 |
5️ Récupérer les en-têtes du tableau:
for row in rows:
cols = row.find_all('th')
print(cols)
if len(cols) > 0 :
cols = [ele.get_text(separator=' ').strip().title() for ele in cols]
columns_participants = cols
[<th scope="col">Club
</th>, <th scope="col">Dernière<br/>montée
</th>, <th scope="col">Budget<sup class="reference" id="cite_ref-3"><a href="#cite_note-3"><span class="cite_crochet">[</span>3<span class="cite_crochet">]</span></a></sup><br/>en M<a href="/wiki/Euro" title="Euro">€</a>
</th>, <th scope="col">Classement<br/><a href="/wiki/Championnat_de_France_de_football_2018-2019" title="Championnat de France de football 2018-2019">2018-2019</a>
</th>, <th scope="col">Entraîneur
</th>, <th scope="col">Depuis
</th>, <th scope="col">Stade
</th>, <th scope="col">Capacité<br/>en L1<sup class="reference" id="cite_ref-4"><a href="#cite_note-4"><span class="cite_crochet">[</span>4<span class="cite_crochet">]</span></a></sup>
</th>, <th scope="col">Nombre<br/>de saisons<br/>en L1
</th>]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
[]
columns_participants
['Club',
'Dernière Montée',
'Budget [ 3 ] En M €',
'Classement 2018-2019',
'Entraîneur',
'Depuis',
'Stade',
'Capacité En L1 [ 4 ]',
'Nombre De Saisons En L1']
6️ Finalisation du tableau
data_participants.columns = columns_participants[1:]
data_participants.head()
Dernière Montée | Budget [ 3 ] En M € | Classement 2018-2019 | Entraîneur | Depuis | Stade | Capacité En L1 [ 4 ] | Nombre De Saisons En L1 | |
---|---|---|---|---|---|---|---|---|
Paris Saint-Germain | 1974 | 637 | 1er | Thomas Tuchel | 2018 | Parc des Princes | 47 929 | 46 |
LOSC Lille | 2000 | 120 | 2e | Christophe Galtier | 2017 | Stade Pierre-Mauroy | 49 712 | 59 |
Olympique lyonnais | 1989 | 310 | 3e | Rudi Garcia | 2019 | Groupama Stadium | 57 206 | 60 |
AS Saint-Étienne | 2004 | 100 | 4e | Claude Puel | 2019 | Stade Geoffroy-Guichard | 41 965 | 66 |
Olympique de Marseille | 1996 | 110 | 5e | André Villas-Boas | 2019 | Orange Vélodrome | 66 226 | 69 |
Pour aller plus loin
Récupération des localisations des stades
Essayez de comprendre pas à pas ce qui est fait dans les étapes qui suivent (la récupération d’informations supplémentaires en naviguant dans les pages des différents clubs).
import urllib
import pandas as pd
import bs4
division=[]
equipe=[]
stade=[]
latitude_stade=[]
longitude_stade=[]
def dms2dd(degrees, minutes, seconds, direction):
dd = float(degrees) + float(minutes)/60 + float(seconds)/(60*60);
if direction == 'S' or direction == 'O':
dd *= -1
return dd;
url_list=["http://fr.wikipedia.org/wiki/Championnat_de_France_de_football_2019-2020", "http://fr.wikipedia.org/wiki/Championnat_de_France_de_football_de_Ligue_2_2019-2020"]
for url_ligue in url_list :
print(url_ligue)
sock = urllib.request.urlopen(url_ligue).read()
page=bs4.BeautifulSoup(sock)
# Rechercher les liens des équipes dans la liste disponible sur wikipedia
for team in page.findAll('span' , {'class' : 'toponyme'}) :
# Indiquer si c'est de la ligue 1 ou de la ligue 2
if url_ligue==url_list[0] :
division.append("L1")
else :
division.append("L2")
# Trouver le nom et le lien de l'équipe
if team.find('a')!=None :
team_url=team.find('a').get('href')
name_team=team.find('a').get('title')
equipe.append(name_team)
url_get_info = "http://fr.wikipedia.org"+team_url
print(url_get_info)
# aller sur la page de l'équipe
search = urllib.request.urlopen(url_get_info).read()
search_team=bs4.BeautifulSoup(search)
# trouver le stade
compteur = 0
for stadium in search_team.findAll('tr'):
for x in stadium.findAll('th' , {'scope' : 'row'} ) :
if x.contents[0].string=="Stade" and compteur == 0:
compteur = 1
# trouver le lien du stade et son nom
url_stade=stadium.findAll('a')[1].get('href')
name_stadium=stadium.findAll('a')[1].get('title')
stade.append(name_stadium)
url_get_stade = "http://fr.wikipedia.org"+url_stade
print(url_get_stade)
# Aller sur la page du stade et trouver ses coodronnées géographiques
search_stade = urllib.request.urlopen(url_get_stade).read()
soup_stade=bs4.BeautifulSoup(search_stade)
kartographer = soup_stade.find('a',{'class': "mw-kartographer-maplink"})
if kartographer == None :
latitude_stade.append(None)
longitude_stade.append(None)
else :
for coordinates in kartographer :
print(coordinates)
liste = coordinates.split(",")
latitude_stade.append(str(liste[0]).replace(" ", "") + "'")
longitude_stade.append(str(liste[1]).replace(" ", "") + "'")
dict = {'division' : division , 'equipe': equipe, 'stade': stade, 'latitude': latitude_stade, 'longitude' : longitude_stade}
data = pd.DataFrame(dict)
data = data.dropna()
http://fr.wikipedia.org/wiki/Championnat_de_France_de_football_2019-2020
http://fr.wikipedia.org/wiki/Paris_Saint-Germain_Football_Club
http://fr.wikipedia.org/wiki/Parc_des_Princes
48° 50′ 29″ N, 2° 15′ 11″ E
http://fr.wikipedia.org/wiki/LOSC_Lille
http://fr.wikipedia.org/wiki/Stade_Pierre-Mauroy
50° 36′ 43″ N, 3° 07′ 50″ E
http://fr.wikipedia.org/wiki/Olympique_lyonnais
http://fr.wikipedia.org/wiki/Parc_Olympique_lyonnais
45° 45′ 55″ N, 4° 58′ 55″ E
http://fr.wikipedia.org/wiki/Association_sportive_de_Saint-%C3%89tienne
http://fr.wikipedia.org/wiki/Stade_Geoffroy-Guichard
45° 27′ 39″ N, 4° 23′ 25″ E
http://fr.wikipedia.org/wiki/Olympique_de_Marseille
http://fr.wikipedia.org/wiki/Orange_V%C3%A9lodrome
43° 16′ 11″ N, 5° 23′ 45″ E
http://fr.wikipedia.org/wiki/Montpellier_H%C3%A9rault_Sport_Club
http://fr.wikipedia.org/wiki/Stade_de_la_Mosson
43° 37′ 19″ N, 3° 48′ 44″ E
http://fr.wikipedia.org/wiki/Stade_de_Reims
http://fr.wikipedia.org/wiki/Stade_Auguste-Delaune
49° 14′ 48″ N, 4° 01′ 30″ E
http://fr.wikipedia.org/wiki/Olympique_Gymnaste_Club_Nice
http://fr.wikipedia.org/wiki/Allianz_Riviera
43° 42′ 18″ N, 7° 11′ 33″ E
http://fr.wikipedia.org/wiki/N%C3%AEmes_Olympique
http://fr.wikipedia.org/wiki/Stade_des_Antonins
43° 48′ 39″ N, 4° 21′ 23″ E
http://fr.wikipedia.org/wiki/Racing_Club_de_Strasbourg_Alsace
http://fr.wikipedia.org/wiki/Stade_de_la_Meinau
48° 33′ 36″ N, 7° 45′ 18″ E
http://fr.wikipedia.org/wiki/Stade_rennais_Football_Club
http://fr.wikipedia.org/wiki/Roazhon_Park
48° 06′ 27″ N, 1° 42′ 46″ O
http://fr.wikipedia.org/wiki/Angers_sporting_club_de_l%27Ouest
http://fr.wikipedia.org/wiki/Stade_Raymond-Kopa
47° 27′ 38″ N, 0° 31′ 51″ O
http://fr.wikipedia.org/wiki/Football_Club_de_Metz
http://fr.wikipedia.org/wiki/Stade_Saint-Symphorien
49° 06′ 35″ N, 6° 09′ 33″ E
http://fr.wikipedia.org/wiki/Football_Club_de_Nantes
http://fr.wikipedia.org/wiki/Stade_de_la_Beaujoire
47° 15′ 20″ N, 1° 31′ 31″ O
http://fr.wikipedia.org/wiki/Stade_brestois_29
http://fr.wikipedia.org/wiki/Stade_Francis-Le_Bl%C3%A9
48° 24′ 11″ N, 4° 27′ 42″ O
http://fr.wikipedia.org/wiki/Football_Club_des_Girondins_de_Bordeaux
http://fr.wikipedia.org/wiki/Matmut_Atlantique
44° 53′ 50″ N, 0° 33′ 41″ O
http://fr.wikipedia.org/wiki/Toulouse_Football_Club
http://fr.wikipedia.org/wiki/Stadium_de_Toulouse
43° 35′ 00″ N, 1° 26′ 03″ E
http://fr.wikipedia.org/wiki/Amiens_Sporting_Club
http://fr.wikipedia.org/wiki/Stade_de_la_Licorne
49° 53′ 38″ N, 2° 15′ 49″ E
http://fr.wikipedia.org/wiki/Association_sportive_de_Monaco_football_club
http://fr.wikipedia.org/wiki/Stade_Louis-II
43° 43′ 39″ N, 7° 24′ 56″ E
http://fr.wikipedia.org/wiki/Dijon_Football_C%C3%B4te-d%27Or
http://fr.wikipedia.org/wiki/Stade_Gaston-G%C3%A9rard
47° 19′ 28″ N, 5° 04′ 06″ E
http://fr.wikipedia.org/wiki/Championnat_de_France_de_football_de_Ligue_2_2019-2020
http://fr.wikipedia.org/wiki/Athletic_Club_ajaccien
http://fr.wikipedia.org/wiki/Stade_Fran%C3%A7ois_Coty
41° 55′ 51″ N, 8° 46′ 35″ E
http://fr.wikipedia.org/wiki/Association_sportive_Nancy-Lorraine
http://fr.wikipedia.org/wiki/Stade_Marcel-Picot
48° 41′ 43″ N, 6° 12′ 38″ E
http://fr.wikipedia.org/wiki/Le_Havre_Athletic_Club_(football)
http://fr.wikipedia.org/wiki/Stade_Oc%C3%A9ane
49° 29′ 56″ N, 0° 10′ 11″ E
http://fr.wikipedia.org/wiki/Stade_Malherbe_Caen_Calvados_Basse-Normandie
http://fr.wikipedia.org/wiki/Stade_Michel-d%27Ornano
49° 10′ 46″ N, 0° 23′ 48″ O
http://fr.wikipedia.org/wiki/Clermont_Foot_63
http://fr.wikipedia.org/wiki/Stade_Gabriel-Montpied
45° 48′ 57″ N, 3° 07′ 18″ E
http://fr.wikipedia.org/wiki/En_Avant_de_Guingamp
http://fr.wikipedia.org/wiki/Stade_de_Roudourou
48° 33′ 58″ N, 3° 09′ 52″ O
http://fr.wikipedia.org/wiki/Football_Club_Lorient
http://fr.wikipedia.org/wiki/Stade_du_Moustoir
47° 44′ 56″ N, 3° 22′ 09″ O
http://fr.wikipedia.org/wiki/Paris_Football_Club
http://fr.wikipedia.org/wiki/Stade_Charl%C3%A9ty
48° 49′ 07″ N, 2° 20′ 47″ E
http://fr.wikipedia.org/wiki/La_Berrichonne_de_Ch%C3%A2teauroux
http://fr.wikipedia.org/wiki/Stade_Gaston-Petit
46° 48′ 07″ N, 1° 43′ 18″ E
http://fr.wikipedia.org/wiki/Association_de_la_jeunesse_auxerroise
http://fr.wikipedia.org/wiki/Stade_de_l%27Abb%C3%A9-Deschamps
47° 47′ 12″ N, 3° 35′ 19″ E
http://fr.wikipedia.org/wiki/Union_sportive_Orl%C3%A9ans_Loiret_football
http://fr.wikipedia.org/wiki/Stade_de_la_Source
47° 50′ 25″ N, 1° 56′ 28″ E
http://fr.wikipedia.org/wiki/Valenciennes_Football_Club
http://fr.wikipedia.org/wiki/Stade_du_Hainaut
50° 20′ 55″ N, 3° 31′ 56″ E
http://fr.wikipedia.org/wiki/Chamois_niortais_Football_Club
http://fr.wikipedia.org/wiki/Stade_Ren%C3%A9-Gaillard
46° 19′ 01″ N, 0° 29′ 21″ O
http://fr.wikipedia.org/wiki/Grenoble_Foot_38
http://fr.wikipedia.org/wiki/Stade_des_Alpes
45° 11′ 15″ N, 5° 44′ 24″ E
http://fr.wikipedia.org/wiki/Football_Club_Sochaux-Montb%C3%A9liard
http://fr.wikipedia.org/wiki/Stade_Auguste-Bonal
47° 30′ 44″ N, 6° 48′ 41″ E
http://fr.wikipedia.org/wiki/Rodez_Aveyron_Football
http://fr.wikipedia.org/wiki/Stade_Paul-Lignon
44° 21′ 06″ N, 2° 33′ 49″ E
http://fr.wikipedia.org/wiki/Football_Club_de_Chambly_Oise
http://fr.wikipedia.org/wiki/Stade_Walter_Luzi
49° 10′ 45″ N, 2° 14′ 01″ E
http://fr.wikipedia.org/wiki/Esp%C3%A9rance_sportive_Troyes_Aube_Champagne
http://fr.wikipedia.org/wiki/Stade_de_l%27Aube
48° 18′ 27″ N, 4° 05′ 55″ E
http://fr.wikipedia.org/wiki/Racing_Club_de_Lens
http://fr.wikipedia.org/wiki/Stade_Bollaert-Delelis
50° 25′ 58″ N, 2° 48′ 54″ E
http://fr.wikipedia.org/wiki/Le_Mans_Football_Club
http://fr.wikipedia.org/wiki/Stade_Marie-Marvingt
47° 57′ 32″ N, 0° 13′ 29″ E
data.head(5)
division | equipe | stade | latitude | longitude | |
---|---|---|---|---|---|
0 | L1 | Paris Saint-Germain Football Club | Parc des Princes | 48° 50′ 29″ N' | 2° 15′ 11″ E' |
1 | L1 | LOSC Lille | Stade Pierre-Mauroy | 50° 36′ 43″ N' | 3° 07′ 50″ E' |
2 | L1 | Olympique lyonnais | Parc Olympique lyonnais | 45° 45′ 55″ N' | 4° 58′ 55″ E' |
3 | L1 | Association sportive de Saint-Étienne | Stade Geoffroy-Guichard | 45° 27′ 39″ N' | 4° 23′ 25″ E' |
4 | L1 | Olympique de Marseille | Orange Vélodrome | 43° 16′ 11″ N' | 5° 23′ 45″ E' |
On va transformer les coordonnées en degrés en coordonnées numériques afin d’être en mesure de faire une carte
import re
def dms2dd(degrees, minutes, seconds, direction):
dd = float(degrees) + float(minutes)/60 + float(seconds)/(60*60);
if direction in ('S', 'O'):
dd *= -1
return dd
def parse_dms(dms):
parts = re.split('[^\d\w]+', dms)
lat = dms2dd(parts[0], parts[1], parts[2], parts[3])
#lng = dms2dd(parts[4], parts[5], parts[6], parts[7])
return lat
data['latitude'] = data['latitude'].apply(parse_dms)
data['longitude'] = data['longitude'].apply(parse_dms)
Tous les éléments sont en place pour faire une belle carte à ce stade. On
va utiliser folium
pour celle-ci, qui est présenté dans la partie
visualisation.
Carte des stades avec folium
#!pip install geopandas
import geopandas as gpd
from pathlib import Path
import folium
gdf = gpd.GeoDataFrame(
data, geometry=gpd.points_from_xy(data.longitude, data.latitude))
Path("leaflet").mkdir(parents=True, exist_ok=True)
center = gdf[['latitude', 'longitude']].mean().values.tolist()
sw = gdf[['latitude', 'longitude']].min().values.tolist()
ne = gdf[['latitude', 'longitude']].max().values.tolist()
m = folium.Map(location = center, tiles='Stamen Toner')
# I can add marker one by one on the map
for i in range(0,len(gdf)):
folium.Marker([gdf.iloc[i]['latitude'], gdf.iloc[i]['longitude']], popup=gdf.iloc[i]['stade']).add_to(m)
m.fit_bounds([sw, ne])
La carte obtenue doit ressembler à la suivante:
Récupérer des informations sur les pokemons
Le prochain exercice pour mettre en pratique le webscraping consiste à récupérer des informations sur les pokemons à partir du site internet pokemondb.net.
Version non guidée
Exercice 2 : Les pokemon (version non guidée)
Pour cet exercice, nous vous demandons d’obtenir différentes informations sur les pokémons :
- les informations personnelles des 893 pokemons sur le site internet pokemondb.net.
Les informations que nous aimerions obtenir au final dans un
DataFrame
sont celles contenues dans 4 tableaux :
- Pokédex data
- Training
- Breeding
- Base stats
- Nous aimerions que vous récupériez également les images de chacun des pokémons et que vous les enregistriez dans un dossier
- Petit indice : utilisez les modules
request
etshutil
- Pour cette question, il faut que vous cherchiez de vous même certains éléments, tout n’est pas présent dans le TD.
Pour la question 1, l’objectif est d’obtenir le code source d’un tableau comme celui qui suit (Pokemon Nincada.)
Pokédex data
National № | 290 |
---|---|
Type | Bug Ground |
Species | Trainee Pokémon |
Height | 0.5 m (1′08″) |
Weight | 5.5 kg (12.1 lbs) |
Abilities |
1. Compound Eyes Run Away (hidden ability) |
Local № |
042 (Ruby/Sapphire/Emerald) 111 (X/Y --- Central Kalos) 043 (Omega Ruby/Alpha Sapphire) 104 (Sword/Shield) |
Training
EV yield | 1 Defense |
---|---|
Catch rate | 255 (33.3% with PokéBall, full HP) |
Base Friendship | 70 (normal) |
Base Exp. | 53 |
Growth Rate | Erratic |
Breeding
Egg Groups | Bug |
---|---|
Gender | 50% male, 50% female |
Egg cycles | 15 (3,599--3,855 steps) |
Base stats
HP | 31 | 172 | 266 | |
---|---|---|---|---|
Attack | 45 | 85 | 207 | |
Defense | 90 | 166 | 306 | |
Sp. Atk | 30 | 58 | 174 | |
Sp. Def | 30 | 58 | 174 | |
Speed | 40 | 76 | 196 | |
Total | 266 | Min | Max |
Pour la question 2, l’objectif est d’obtenir l’une des images suivantes:
Version guidée
Les prochaines parties permettront de faire l’exercice ci-dessus étape par étape, de manière guidée.
Nous souhaitons tout d’abord obtenir les informations personnelles de tous les pokemons sur pokemondb.net.
Les informations que nous aimerions obtenir au final pour les pokemons sont celles contenues dans 4 tableaux :
- Pokédex data
- Training
- Breeding
- Base stats
Nous proposons ensuite de récupérer et afficher les images.
Etape 1: constituer un DataFrame de caractéristiques
Exercice 2b : Les pokémons (version guidée)
Pour récupérer les informations, le code devra être divisé en plusieurs étapes :
- Trouvez la page principale du site et la transformer en un objet intelligible pour votre code. Les fonctions suivantes vous seront utiles :
urllib.request.Request
urllib.request.urlopen
bs4.BeautifulSoup
-
Créez une fonction qui permet de récupérer la page d’un pokémon à partir de son nom.
-
A partir de la page de
bulbasaur
, obtenez les 4 tableaux qui nous intéressent :
- on va chercher l’élément suivant :
('table', { 'class' : "vitals-table"})
- puis stocker ses éléments dans un dictionnaire
-
Récupérez par ailleurs la liste de noms des pokémons qui nous permettra de faire une boucle par la suite. Combien trouvez-vous de pokémons ?
-
Ecrire une fonction qui récupère l’ensemble des informations sur les dix premiers pokémons de la liste et les intègre dans un
DataFrame
A l’issue de la question 3, vous devriez obtenir une liste de caractéristiques proche de celle-ci:
defaultdict(None,
{'National №': '001',
'name': 'bulbasaur',
'Type': ' Grass Poison ',
'Species': 'Seed Pokémon',
'Height': '0.7\xa0m (2′04″)',
'Weight': '6.9\xa0kg (15.2\xa0lbs)',
'Abilities': '1. OvergrowChlorophyll (hidden ability)',
'Local №': "001 (Red/Blue/Yellow)226 (Gold/Silver/Crystal)001 (FireRed/LeafGreen)231 (HeartGold/SoulSilver)080 (X/Y — Central Kalos)001 (Let's Go Pikachu/Let's Go Eevee)068 (The Isle of Armor)",
'EV yield': ' 1 Special Attack ',
'Catch rate': ' 45 (5.9% with PokéBall, full HP) ',
'Base Friendship': ' 50 (normal) ',
'Base Exp.': '64',
'Growth Rate': 'Medium Slow',
'Egg Groups': 'Grass, Monster',
'Gender': '87.5% male, 12.5% female',
'Egg cycles': '20 (4,884–5,140 steps) ',
'HP': '45',
'Attack': '49',
'Defense': '49',
'Sp. Atk': '65',
'Sp. Def': '65',
'Speed': '45'})
La structure est ici en dictionnaire, ce qui est pratique.
Enfin, vous les
informations sur les dix premiers pokémons de la liste intégrées dans un
DataFrame
prendront l’aspect suivant:
National № | name | Type | Species | Height | Weight | Abilities | Local № | EV yield | Catch rate | ... | Growth Rate | Egg Groups | Gender | Egg cycles | HP | Attack | Defense | Sp. Atk | Sp. Def | Speed | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 001 | bulbasaur | Grass Poison | Seed Pokémon | 0.7 m (2′04″) | 6.9 kg (15.2 lbs) | 1. OvergrowChlorophyll (hidden ability) | 001 (Red/Blue/Yellow)226 (Gold/Silver/Crystal)... | 1 Special Attack | 45 (5.9% with PokéBall, full HP) | ... | Medium Slow | Grass, Monster | 87.5% male, 12.5% female | 20 (4,884–5,140 steps) | 45 | 49 | 49 | 65 | 65 | 45 |
1 | 002 | ivysaur | Grass Poison | Seed Pokémon | 1.0 m (3′03″) | 13.0 kg (28.7 lbs) | 1. OvergrowChlorophyll (hidden ability) | 002 (Red/Blue/Yellow)227 (Gold/Silver/Crystal)... | 1 Special Attack, 1 Special Defense | 45 (5.9% with PokéBall, full HP) | ... | Medium Slow | Grass, Monster | 87.5% male, 12.5% female | 20 (4,884–5,140 steps) | 60 | 62 | 63 | 80 | 80 | 60 |
2 | 003 | venusaur | Grass Poison | Seed Pokémon | 2.0 m (6′07″) | 100.0 kg (220.5 lbs) | 1. OvergrowChlorophyll (hidden ability) | 003 (Red/Blue/Yellow)228 (Gold/Silver/Crystal)... | 2 Special Attack, 1 Special Defense | 45 (5.9% with PokéBall, full HP) | ... | Medium Slow | Grass, Monster | 87.5% male, 12.5% female | 20 (4,884–5,140 steps) | 80 | 82 | 83 | 100 | 100 | 80 |
3 | 004 | charmander | Fire | Lizard Pokémon | 0.6 m (2′00″) | 8.5 kg (18.7 lbs) | 1. BlazeSolar Power (hidden ability) | 004 (Red/Blue/Yellow)229 (Gold/Silver/Crystal)... | 1 Speed | 45 (5.9% with PokéBall, full HP) | ... | Medium Slow | Dragon, Monster | 87.5% male, 12.5% female | 20 (4,884–5,140 steps) | 39 | 52 | 43 | 60 | 50 | 65 |
4 | 005 | charmeleon | Fire | Flame Pokémon | 1.1 m (3′07″) | 19.0 kg (41.9 lbs) | 1. BlazeSolar Power (hidden ability) | 005 (Red/Blue/Yellow)230 (Gold/Silver/Crystal)... | 1 Special Attack, 1 Speed | 45 (5.9% with PokéBall, full HP) | ... | Medium Slow | Dragon, Monster | 87.5% male, 12.5% female | 20 (4,884–5,140 steps) | 58 | 64 | 58 | 80 | 65 | 80 |
5 rows × 22 columns
Etape 2: récupérer et afficher des photos de Pokemon
Nous aimerions que vous récupériez également les images des 5 premiers pokémons et que vous les enregistriez dans un dossier.
Exercice 2b : Les pokémons (version guidée)
- Les URL des images des pokemon prennent la forme “https://img.pokemondb.net/artwork/{pokemon}.jpg".
Utiliser les modules
requests
etshutil
pour télécharger et enregistrer en local les images. - Importer ces images stockées au format JPEG dans
Python
grâce à la fonctionimread
du packageskimage.io

Selenium
: mimer le comportement d’un utilisateur internet
Jusqu’à présent, nous avons raisonné comme si nous connaissions toujours l’url qui nous intéresse. De plus, les pages que nous visitons sont “statiques”, elles ne dépendent pas d’une action ou d’une recherche de l’internaute.
Nous allons voir à présent comment nous en sortir pour remplir
des champs sur un site web et récupérer ce qui nous intéresse.
La réaction d’un site web à l’action d’un utilisateur passe régulièrement par
l’usage de JavaScript
dans le monde du développement web.
Le package Selenium permet
de reproduire, depuis un code automatisé, le comportement
manuel d’un utilisateur. Il permet ainsi
d’obtenir des informations du site qui ne sont pas dans le
code HTML
mais qui apparaissent uniquement à la suite de
l’exécution de script JavaScript
en arrière plan.
Selenium
se comporte comme un utilisateur lambda sur internet :
il clique sur des liens, il remplit des formulaires, etc.
Premier exemple en scrapant un moteur de recherche
Dans cet exemple, nous allons essayer d’aller sur le site de Bing Actualités et entrer dans la barre de recherche un sujet donné. Pour tester, nous allons faire une recherche avec le mot-clé “Trump”.
L’installation de Selenium
nécessite d’avoir Chromium
qui est un
navigateur Google Chrome minimaliste.
La version de chromedriver
doit être >= 2.36
et dépend de la version de Chrome
que vous avez sur votre environnement
de travail. Pour installer cette version minimaliste de Chrome
sur un environnement
Linux
, vous pouvez
vous référer à l’encadré dédié
`Installation de Selenium`
D’abord, il convient d’installer les dépendances.
Sur Colab
, vous pouvez utiliser les commandes suivantes:
!sudo apt-get update
!sudo apt install -y unzip xvfb libxi6 libgconf-2-4 -y
!sudo apt install chromium-chromedriver -y
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
Si vous êtes sur le SSP-Cloud
, vous pouvez
exécuter les commandes suivantes:
!wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -O /tmp/chrome.deb
!sudo apt-get update
!sudo -E apt-get install -y /tmp/chrome.deb
!pip install chromedriver-autoinstaller selenium
import chromedriver_autoinstaller
chromedriver_autoinstaller.install()
Vous pouvez ensuite installer Selenium
. Par
exemple, depuis une
cellule de Notebook
:
!pip install selenium
Après avoir installé Chromium
,
il est nécessaire d’indiquer à Python
où
le trouver. Si vous êtes sur Linux
et que vous
avez suivi les consignes précédentes, vous
pouvez faire:
import sys
sys.path.insert(0,'/usr/lib/chromium-browser/chromedriver')
import selenium
path_to_web_driver = "chromedriver"
En premier lieu, il convient d’initialiser le comportement
de Selenium
en répliquant les paramètres
du navigateur. Pour cela, on va d’abord initialiser
notre navigateur avec quelques options:
import time
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
#chrome_options.add_argument('--verbose')
Puis on lance le navigateur:
browser = webdriver.Chrome(executable_path=path_to_web_driver,
options=chrome_options)
On va sur le site de Bing Actualités
, et on lui indique le mot clé que nous souhaitons chercher.
En l’occurrence, on s’intéresse aux actualités de Donald Trump.
Après avoir inspecté la page depuis les outils de développement du navigateur,
on voit que la barre de recherche est un élement du code appelé q
(comme query).
On va ainsi demander à selenium
de chercher cet élément:
browser.get('https://www.bing.com/news')
search = browser.find_element("name", "q")
print(search)
print([search.text, search.tag_name, search.id])
# on envoie à cet endroit le mot qu'on aurait tapé dans la barre de recherche
search.send_keys("Trump")
search_button = browser.find_element("xpath", "//input[@id='sb_form_go']")
search_button.click()
<selenium.webdriver.remote.webelement.WebElement (session="70d55e363dbfb6b295e64b1bacb6f871", element="c62b23a2-a64f-4a4e-b0e9-4876c9a0928e")>
['', 'input', 'c62b23a2-a64f-4a4e-b0e9-4876c9a0928e']
selenium
permet de capturer l’image qu’on verrait dans le navigateur
avec get_screenshot_as_png
. Cela peut être utile pour vérifier qu’on
a fait la bonne action:
png = browser.get_screenshot_as_png()
from IPython.display import Image
Image(png, width='500')
Enfin, on peut extraire les résultats. Plusieurs
méthodes sont disponibles. La méthode la plus
pratique, lorsqu’elle est disponible,
est d’utiliser le XPath
qui est un chemin
non ambigu pour accéder à un élement. En effet,
plusieurs éléments peuvent partager la même classe ou
le même attribut ce qui peut faire qu’une recherche
de ce type peut renvoyer plusieurs échos.
Pour déterminer le XPath
d’un objet, les outils
de développeurs de votre site web sont pratiques.
Par exemple, sous Firefox
, une fois que vous
avez trouvé un élément dans l’inspecteur, vous
pouvez faire click droit > Copier > XPath
.
from selenium.common.exceptions import StaleElementReferenceException
links = browser.find_elements("xpath", "//div/a[@class='title'][@href]")
results = []
for link in links:
try:
url = link.get_attribute('href')
except StaleElementReferenceException as e:
print("Issue with '{0}' and '{1}'".format(url, link))
print("It might be due to slow javascript which produces the HTML page.")
results.append(url)
Enfin, pour mettre fin à notre session, on demande
à Python
de quitter le navigateur
browser.quit()
On a obtenu les résultats suivants:
print(results)
['https://www.newsweek.com/trump-lawyer-alina-habba-jean-carroll-1778049', 'https://news.yahoo.com/stormy-daniels-thanks-trump-accidentally-073604317.html', 'https://news.yahoo.com/damning-montage-exposes-donald-trump-073926486.html', 'https://www.foxnews.com/shows/media-buzz/republicans-privately-gripe-about-trump-dont-dare-take-him-on', 'https://www.nytimes.com/2023/01/31/us/politics/trump-fundraising-2024-campaign.html', 'https://www.msn.com/en-us/news/other/news-outlets-raced-to-publish-trump-s-lawsuit-against-woodward-experts-say-the-suit-has-no-legal-merit-whatsoever/ar-AA16YCU7', 'https://www.msn.com/en-us/news/politics/trump-committee-burns-through-cash-in-early-months-new-filings-show/ar-AA16XtY8', 'https://www.msn.com/en-us/news/politics/trump-desantis-battle-turns-into-a-covid-war/ar-AA16XOIR', 'https://www.nbcnews.com/politics/2024-election/trump-vows-stop-gender-affirming-care-minors-re-elected-president-rcna68461', 'https://www.reuters.com/world/us/trumps-fundraising-presidential-bid-gets-off-modest-start-2023-02-01/', 'https://www.msn.com/en-us/news/politics/trump-vows-to-end-left-wing-gender-insanity-if-re-elected/ar-AA16YcFl', 'https://www.msn.com/en-us/video/news/netanyahu-touts-trumps-wins-with-israel-but-points-out-one-big-mistake/vi-AA16Yump']
Les autres méthodes utiles de Selenium
:
find_element(****).click()
| Une fois qu’on a trouvé un élément réactif, notamment un bouton, on peut cliquer dessus pour activer une nouvelle page |
find_element(****).send_keys("toto")
| Une fois qu’on a trouvé un élément, notamment un champ où s’authentifier, on peut envoyer une valeur, ici “toto”.
Utiliser selenium pour jouer à 2048
Dans cet exemple, on utilise le module pour que Python
appuie lui même sur les touches du clavier afin de jouer à 2048.
Note : ce bout de code ne donne pas une solution à 2048,
il permet juste de voir ce qu’on peut faire avec Selenium
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
# on ouvre la page internet du jeu 2048
browser = webdriver.Chrome(executable_path=path_to_web_driver,
options=chrome_options)
browser.get('https://play2048.co//')
# Ce qu'on va faire : une boucle qui répète inlassablement la même chose : haut / droite / bas / gauche
# on commence par cliquer sur la page pour que les touches sachent
browser.find_element("class name", 'grid-container').click()
grid = browser.find_element("tag name", 'body')
# pour savoir quels coups faire à quel moment, on crée un dictionnaire
direction = {0: Keys.UP, 1: Keys.RIGHT, 2: Keys.DOWN, 3: Keys.LEFT}
count = 0
while True:
try: # on vérifie que le bouton "Try again" n'est pas là - sinon ça veut dire que le jeu est fini
retryButton = browser.find_element("link text",'Try again')
scoreElem = browser.find_element("class name", 'score-container')
break
except:
#Do nothing. Game is not over yet
pass
# on continue le jeu - on appuie sur la touche suivante pour le coup d'après
count += 1
grid.send_keys(direction[count % 4])
time.sleep(0.1)
print('Score final : {} en {} coups'.format(scoreElem.text, count))
browser.quit()
Exercices supplémentaires
Récupérer les noms et âges des ministres français
Pour cet exercice, on propose de scraper la liste des ministres français depuis le site du gouvernement. L’objectif sera, in fine de faire un graphique qui représente la distribution de leurs âges. La solution pour cet exercice a été proposée par Tien-Thinh et Antoine Palazzolo.
Pour être en mesure de faire cet exercice, il est
recommandé d’installer le package dateparser
!pip install dateparser
#depuis un notebook. En ligne de commande, retirer le !
Pour cet exercice, nous proposons d’utiliser les packages suivants:
import time
from tqdm import tqdm
import urllib
import re, datetime
from dateutil.parser import parse as parse_dt
import dateparser
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import bs4
Nous proposons également d’utiliser la fonction suivante pour calculer l’âge à partir de la date de naissance.
def from_birth_to_age(birth):
today = datetime.date.today()
return today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
Exercice : Les ministres français
- Créer des variables globales
url_gouvernement
eturl_gouvernement
qui représenteront, respectivement, la racine de l’URL du site web et le chemin au sein de celui-ci ; - Utiliser
bs4
pour récupérer la composition du gouvernement, qui est contenue dans un<div>
ayant une classe ad hoc. Nommer cet objetcompo
- Utiliser
find_all
pour récupérer la liste des ministres danscompo
. Nommer cet objetministres
- Inspecter la structure des champs au sein de
ministres
. Répérer les idbiography
. Comme la structure est générique, on va écrire une fonctionfrom_bio_to_age
sur laquelle on va itérer pour chaque élément de la listeministres
. Cette fonction effectuera les opérations suivantes:- Remplacer les champs de dates de naissance non numériques (par exemple “1er”), en valeur numérique (par exemple 1).
- Utiliser la regex
[0-3]?\d \S* \d{4}
avec le packagere
pour extraire les dates de naissance. Nommer l’objetstr_date
. - Appliquer
dateparser.parse
pour convertir sous forme de date - Appliquer
from_birth_to_age
pour transformer cette date de naissance en âge
- Pour chaque élément de la liste
ministres
, faire une boucle (en introduisant untime.sleep(0.25)
entre chaque itération pour ne pas surcharger le site):- Récupérer les noms et prénoms, fonctions pour chaque ministre
- Récupérer l’URL de la photo
- Créer un URL pour chaque ministre afin d’appliquer la fonction
from_bio_to_age
- Utiliser
matplotlib
ouseaborn
pour faire un histogramme d’âge
A l’issue de la question 4, on devrait retrouver les informations suivantes:
print(f"Nous retrouvons ainsi {len(ministres)} ministres.")
Nous retrouvons ainsi 44 ministres.
def from_bio_to_age(url):
html = urllib.request.urlopen(url).read()
page = bs4.BeautifulSoup(html)
s = page.find("div", {"id":"biography"}).text.replace("1er", "1") # un peu ad hoc
expression = re.compile("[0-3]?\d \S* \d{4}") # renvoie parfois des dates autres que dates de naissance
str_date = expression.findall(s)[0]
date_de_naissance = dateparser.parse(str_date).date()
return from_birth_to_age(date_de_naissance)
In fine, on obtient une liste dont le premier élément prend la forme suivante:
liste[0]
{'Nom complet': 'Élisabeth Borne',
'Fonction': 'Première ministre',
'Photo': 'https://www.gouvernement.fr/sites/default/files/styles/composition_large/public/pm_elisabeth_borne_portrait_matignon_.jpg?itok=lT5QYQhk',
'href': 'https://www.gouvernement.fr/ministre/elisabeth-borne',
'Age': 61}
Finalement, le DataFrame
pourra être
structuré sous la forme suivante. On va éliminer
les âges égaux à 0 sont qui sont des erreurs
de scraping:
lorsque la date de naissance complète n’est pas disponible
sur la biographie d’un ministre.
df = pd.DataFrame(liste)
df = df.loc[df['Age'] != 0]
df.head(3)
Nom complet | Fonction | Photo | href | Age | |
---|---|---|---|---|---|
0 | Élisabeth Borne | Première ministre | https://www.gouvernement.fr/sites/default/file... | https://www.gouvernement.fr/ministre/elisabeth... | 61 |
1 | Olivier Véran | Ministre délégué auprès de la Première ministr... | https://www.gouvernement.fr/sites/default/file... | https://www.gouvernement.fr/ministre/olivier-v... | 42 |
2 | Franck Riester | Ministre délégué auprès de la Première ministr... | https://www.gouvernement.fr/sites/default/file... | https://www.gouvernement.fr/ministre/franck-ri... | 49 |
Finalement, l’histogramme aura l’aspect suivant:
plt.hist(df.Age, bins=np.arange(25, 80, 4))
(array([0., 0., 6., 6., 6., 3., 3., 6., 3., 4., 2., 1., 0.]),
array([25., 29., 33., 37., 41., 45., 49., 53., 57., 61., 65., 69., 73.,
77.]),
<BarContainer object of 13 artists>)
