Web scraping avec Python

Python permet de facilement récupérer une page web pour en extraire des données à restructurer. Le web scraping, que les Canadiens nomment “moissonnage du web”, est une manière de plus en plus utilisée de récupérer une grande masse d’information en temps réel. Ce chapitre présente les deux principaux paradigmes par le biais de BeautifulSoup et Selenium et les principaux défis du web scraping.

Exercice
Manipulation
Author

Lino Galiana

Published

2024-04-27

Le web scraping 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.

1 Enjeux

Un certain nombre d’enjeux du web scraping ne seront évoqués que superficiellement dans le cadre de ce chapitre.

1.1 La zone grise de la légalité du web scraping

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 web scraping 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 web scraping 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 web scraping pour ne pas se mettre en faute légalement.

1.2 Stabilité et fiabilité des informations reçues

La récupération de données par web scraping 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 scrapé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 web scraping. 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 web scraping, 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 web scraping 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 web scraping 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 web scraping est plus proche du comportement dans le Far West.

1.3 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 ne pas être civilisé. En effet, lorsqu’il est non-maîtrisé, le web scraping 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ées 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 à une heure programmée à l’avance, il existe les cronjobs.

2 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

Si vous savez quel élément vous intéresse, vous pouvez également ouvrir l’inspecteur du navigateur (clic droit sur l’élément + “Inspecter”), pour afficher les balises encadrant votre élément de façon plus ergonomique, un peu comme un zoom.

2.1 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>).

Par exemple, les principales balises définissant la structure d’un tableau sont les suivantes :

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 :

Le Titre de mon tableau
Prénom Nom Profession
Mike Stuntman Cascadeur
Mister Pink Gangster

2.1.1 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 :

bla,bla

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.

3 Scraper avec Python: le package BeautifulSoup

3.1 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 “web crawling”. Scrapy est plus complexe à manipuler que BeautifulSoup : si vous voulez plus de détails, rendez-vous sur la page du tutoriel Scrapy.

Le web scraping 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.

!pip install -q lxml

import bs4
import lxml
import pandas
import urllib

from urllib import request
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

Pour faire fonctionner Selenium, il faut utiliser un package nommé webdriver-manager:

!pip install webdriver-manager
Requirement already satisfied: webdriver-manager in /opt/mamba/lib/python3.11/site-packages (4.0.1)
Requirement already satisfied: requests in /opt/mamba/lib/python3.11/site-packages (from webdriver-manager) (2.31.0)
Requirement already satisfied: python-dotenv in /opt/mamba/lib/python3.11/site-packages (from webdriver-manager) (1.0.1)
Requirement already satisfied: packaging in /opt/mamba/lib/python3.11/site-packages (from webdriver-manager) (23.2)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/mamba/lib/python3.11/site-packages (from requests->webdriver-manager) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /opt/mamba/lib/python3.11/site-packages (from requests->webdriver-manager) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/mamba/lib/python3.11/site-packages (from requests->webdriver-manager) (1.26.18)
Requirement already satisfied: certifi>=2017.4.17 in /opt/mamba/lib/python3.11/site-packages (from requests->webdriver-manager) (2024.2.2)
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

3.2 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.

3.3 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 lorsque vous chercher une table,
  • 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_(France)" title="Ligue de football professionnel (France)">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"><span class="mw-image-border noviewer" typeof="mw:File"><a class="mw-file-description" href="/wiki/Fichier:Flag_of_France.svg" title="Drapeau de la France"><img alt="Drapeau de la France" class="mw-file-element" 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> </span><a href="/wiki/France" title="France">France</a></span> et <br/><span class="datasortkey" data-sort-value="Monaco"><span class="flagicon"><span class="mw-image-border noviewer" typeof="mw:File"><a class="mw-file-description" href="/wiki/Fichier:Flag_of_Monaco.svg" title="Drapeau de Monaco"><img alt="Drapeau de Monaco" class="mw-file-element" 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> </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>
<cite class="ouvrage" id="site_officiel" style="font-style: normal;"><a class="external text" href="https://www.ligue1.fr/" rel="nofollow">Site officiel</a></cite></td>
</tr></tbody></table>

ce qui est le texte source permettant de générer le tableau suivant :

Généralités
Sport Football
Organisateur(s) LFP
Édition 82e
Lieu(x) Drapeau de la France France et
Drapeau de Monaco Monaco
Date Du
au (arrêt définitif)
Participants 20 équipes
Matchs joués 279 (sur 380 prévus)
Site web officiel Site officiel

3.4 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>

4 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.

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)
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
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
Montpellier HSC 2009 40 6e Michel Der Zakarian 2017 Stade de la Mosson 22 000 27
OGC Nice 2002 50 7e Patrick Vieira 2018 Allianz Riviera 35 596 60
Stade de Reims 2018 45 8e David Guion 2017 Stade Auguste-Delaune 20 546 35
Nîmes Olympique 2018 27 9e Bernard Blaquart 2015 Stade des Costières 15 788 35
Stade rennais FC 1994 65 10e Julien Stéphan 2018 Roazhon Park 29 194 62
RC Strasbourg Alsace 2017 43 11e Thierry Laurey 2016 Stade de la Meinau 26 109 58
FC Nantes 2013 70 12e Christian Gourcuff 2019 Stade de la Beaujoire - Louis Fonteneau 35 322 51
SCO d’Angers 2015 32 13e Stéphane Moulin 2011 Stade Raymond-Kopa 14 582 27
Girondins de Bordeaux 1992 70 14e Paulo Sousa 2019 Matmut Atlantique 42 115 66
Amiens SC 2017 30 15e Luka Elsner 2019 Stade Crédit Agricole la Licorne 12 999 2
Toulouse FC 2003 35 16e Denis Zanko 2020 Stadium de Toulouse 33 033 32
AS Monaco 2013 220 17e Robert Moreno 2019 Stade Louis-II 16 500 60
Dijon FCO 2016 38 18e Stéphane Jobard 2019 Parc des Sports Gaston-Gérard 15 459 4
FC Metz 2019 40 1er (Ligue 2) Vincent Hognon 2019 Stade Saint-Symphorien 25 865 61
Stade brestois 29 2019 30 2e (Ligue 2) Olivier Dall’Oglio 2019 Stade Francis-Le Blé 14 920 13

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"><span class="mw-image-border noviewer" typeof="mw:File"><a class="mw-file-description" href="/wiki/Fichier:Flag_of_Germany.svg" title="Drapeau : Allemagne"><img alt="" class="mw-file-element" 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></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

5 Pour aller plus loin

5.1 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 = []

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()
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 Stade 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.

5.2 Carte des stades avec folium

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="openstreetmap")

# 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 :

Make this Notebook Trusted to load map: File -> Trust Notebook

6 Récupérer des informations sur les pokemons

Le prochain exercice pour mettre en pratique le web scraping consiste à récupérer des informations sur les pokemons à partir du site internet pokemondb.net.

6.1 Version non guidée

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 les images des pokemon.

6.2 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.

6.2.1 Etape 1: constituer un DataFrame de caractéristiques

A l’issue de la question 3, vous devriez obtenir une liste de caractéristiques proche de celle-ci :

{'National №': '0001',
 '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 №': "0001 (Red/Blue/Yellow)0226 (Gold/Silver/Crystal)0001 (FireRed/LeafGreen)0231 (HeartGold/SoulSilver)0080 (X/Y — Central Kalos)0001 (Let's Go Pikachu/Let's Go Eevee)0068 (The Isle of Armor)0164 (The Indigo Disk)",
 'EV yield': ' 1 Sp. Atk ',
 '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 pouvez intégrer les informations des dix premiers pokémons à un DataFrame, qui aura 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 0001 bulbasaur Grass Poison Seed Pokémon 0.7 m (2′04″) 6.9 kg (15.2 lbs) 1. OvergrowChlorophyll (hidden ability) 0001 (Red/Blue/Yellow)0226 (Gold/Silver/Crysta... 1 Sp. Atk 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 0002 ivysaur Grass Poison Seed Pokémon 1.0 m (3′03″) 13.0 kg (28.7 lbs) 1. OvergrowChlorophyll (hidden ability) 0002 (Red/Blue/Yellow)0227 (Gold/Silver/Crysta... 1 Sp. Atk, 1 Sp. Def 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 0003 venusaur Grass Poison Seed Pokémon 2.0 m (6′07″) 100.0 kg (220.5 lbs) 1. OvergrowChlorophyll (hidden ability) 0003 (Red/Blue/Yellow)0228 (Gold/Silver/Crysta... 2 Sp. Atk, 1 Sp. Def 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 0004 charmander Fire Lizard Pokémon 0.6 m (2′00″) 8.5 kg (18.7 lbs) 1. BlazeSolar Power (hidden ability) 0004 (Red/Blue/Yellow)0229 (Gold/Silver/Crysta... 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 0005 charmeleon Fire Flame Pokémon 1.1 m (3′07″) 19.0 kg (41.9 lbs) 1. BlazeSolar Power (hidden ability) 0005 (Red/Blue/Yellow)0230 (Gold/Silver/Crysta... 1 Sp. Atk, 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

6.2.2 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.

!pip install scikit-image
Requirement already satisfied: scikit-image in /opt/mamba/lib/python3.11/site-packages (0.23.2)
Requirement already satisfied: numpy>=1.23 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (1.26.4)
Requirement already satisfied: scipy>=1.9 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (1.13.0)
Requirement already satisfied: networkx>=2.8 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (3.3)
Requirement already satisfied: pillow>=9.1 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (10.3.0)
Requirement already satisfied: imageio>=2.33 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (2.34.1)
Requirement already satisfied: tifffile>=2022.8.12 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (2024.4.24)
Requirement already satisfied: packaging>=21 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (23.2)
Requirement already satisfied: lazy-loader>=0.4 in /opt/mamba/lib/python3.11/site-packages (from scikit-image) (0.4)
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

7 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.

7.1 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é.

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 selenium
from webdriver_manager.chrome import ChromeDriverManager

path_to_web_driver = ChromeDriverManager().install()

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("--disable-dev-shm-usage")
# chrome_options.add_argument('--verbose')

Puis on lance le navigateur :

from selenium.webdriver.chrome.service import Service

service = Service(executable_path=path_to_web_driver)

browser = webdriver.Chrome(service=service, 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 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 :

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.

Enfin, pour mettre fin à notre session, on demande à Python de quitter le navigateur:

browser.quit()

On a obtenu les résultats suivants :

['https://www.seattletimes.com/nation-world/nation-politics/a-match-made-in-maga-how-a-friendship-helped-jd-vance-land-on-trumps-vp-list/', 'https://www.nytimes.com/2024/04/27/nyregion/david-pecker-trump-tabloids.html', 'https://www.msn.com/en-us/news/politics/coat-hanger-could-unlock-storage-room-where-trump-stored-classified-docs-witness/ar-AA1nLTx5', 'https://www.msn.com/en-us/news/politics/trump-endorses-gop-utah-senate-candidate-looking-to-replace-romney-he-will-be-a-great-senator/ar-AA1nMbNk', 'https://www.msn.com/en-us/news/politics/from-new-york-to-arizona-inside-the-head-spinning-week-of-trumps-legal-drama/ar-AA1nLDuj', 'https://www.msn.com/en-us/news/us/how-justice-amy-coney-barrett-drove-the-supreme-court-s-debate-on-abortion-and-trump-immunity/ar-AA1nLJAo', 'https://www.msn.com/en-us/news/other/trump-on-trial-personal-anguish-political-defiance-and-a-loss-of-control/ar-AA1nMdEG', 'https://www.msn.com/en-us/news/politics/america-deserves-to-see-biden-trump-debate-kristen-hawn/vi-AA1nMgSH']

Les autres méthodes utiles de Selenium:

Méthode Résultat
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”.

7.2 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.chrome.service import Service
from selenium.webdriver.common.keys import Keys

# on ouvre la page internet du jeu 2048
service = Service(executable_path=path_to_web_driver)

browser = webdriver.Chrome(service=service, 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
button = browser.find_element("class name", "grid-container")
browser.execute_script("arguments[0].click();", button)
time.sleep(0.5)

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()

7.3 Exercice supplémentaire

Pour découvrir une autre application possible du web scraping, vous pouvez également vous lancer dans le sujet 5 de l’édition 2023 d’un hackathon non compétitif organisé par l’Insee :

Le contenu de la section NLP du cours pourra vous être utile pour la seconde partie du sujet !

Informations additionnelles

environment files have been tested on.

Latest built version: 2024-04-27

Python version used:

'3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:40:35) [GCC 12.3.0]'
Package Version
affine 2.4.0
aiobotocore 2.12.2
aiohttp 3.9.3
aioitertools 0.11.0
aiosignal 1.3.1
alembic 1.13.1
aniso8601 9.0.1
annotated-types 0.6.0
appdirs 1.4.4
archspec 0.2.3
astroid 3.1.0
asttokens 2.4.1
attrs 23.2.0
Babel 2.14.0
bcrypt 4.1.2
beautifulsoup4 4.12.3
black 24.4.2
blinker 1.7.0
blis 0.7.11
bokeh 3.4.0
boltons 23.1.1
boto3 1.34.51
botocore 1.34.51
branca 0.7.1
Brotli 1.1.0
cachetools 5.3.3
cartiflette 0.0.2
Cartopy 0.23.0
catalogue 2.0.10
cattrs 23.2.3
certifi 2024.2.2
cffi 1.16.0
charset-normalizer 3.3.2
chromedriver-autoinstaller 0.6.4
click 8.1.7
click-plugins 1.1.1
cligj 0.7.2
cloudpathlib 0.16.0
cloudpickle 3.0.0
colorama 0.4.6
comm 0.2.2
commonmark 0.9.1
conda 24.3.0
conda-libmamba-solver 24.1.0
conda-package-handling 2.2.0
conda_package_streaming 0.9.0
confection 0.1.4
contextily 1.6.0
contourpy 1.2.1
cryptography 42.0.5
cycler 0.12.1
cymem 2.0.8
cytoolz 0.12.3
dask 2024.4.1
dask-expr 1.0.10
debugpy 1.8.1
decorator 5.1.1
dill 0.3.8
distributed 2024.4.1
distro 1.9.0
docker 7.0.0
duckdb 0.10.1
en-core-web-sm 3.7.1
entrypoints 0.4
et-xmlfile 1.1.0
exceptiongroup 1.2.0
executing 2.0.1
fastjsonschema 2.19.1
fiona 1.9.6
flake8 7.0.0
Flask 3.0.2
folium 0.16.0
fontawesomefree 6.5.1
fonttools 4.51.0
frozenlist 1.4.1
fsspec 2023.12.2
GDAL 3.8.4
gensim 4.3.2
geographiclib 2.0
geopandas 0.12.2
geoplot 0.5.1
geopy 2.4.1
gitdb 4.0.11
GitPython 3.1.43
google-auth 2.29.0
graphene 3.3
graphql-core 3.2.3
graphql-relay 3.2.0
graphviz 0.20.3
great-tables 0.5.0
greenlet 3.0.3
gunicorn 21.2.0
h11 0.14.0
htmltools 0.5.1
hvac 2.1.0
idna 3.6
imageio 2.34.1
importlib_metadata 7.1.0
importlib_resources 6.4.0
inflate64 1.0.0
ipykernel 6.29.3
ipython 8.22.2
ipywidgets 8.1.2
isort 5.13.2
itsdangerous 2.1.2
jedi 0.19.1
Jinja2 3.1.3
jmespath 1.0.1
joblib 1.3.2
jsonpatch 1.33
jsonpointer 2.4
jsonschema 4.21.1
jsonschema-specifications 2023.12.1
jupyter-cache 1.0.0
jupyter_client 8.6.1
jupyter_core 5.7.2
jupyterlab_widgets 3.0.10
kaleido 0.2.1
kiwisolver 1.4.5
kubernetes 29.0.0
langcodes 3.4.0
language_data 1.2.0
lazy_loader 0.4
libmambapy 1.5.7
llvmlite 0.42.0
locket 1.0.0
lxml 5.2.1
lz4 4.3.3
Mako 1.3.2
mamba 1.5.7
mapclassify 2.6.1
marisa-trie 1.1.0
Markdown 3.6
MarkupSafe 2.1.5
matplotlib 3.8.3
matplotlib-inline 0.1.6
mccabe 0.7.0
menuinst 2.0.2
mercantile 1.2.1
mizani 0.11.2
mlflow 2.11.3
mlflow-skinny 2.11.3
msgpack 1.0.7
multidict 6.0.5
multivolumefile 0.2.3
munkres 1.1.4
murmurhash 1.0.10
mypy 1.9.0
mypy-extensions 1.0.0
nbclient 0.10.0
nbformat 5.10.4
nest_asyncio 1.6.0
networkx 3.3
nltk 3.8.1
numba 0.59.1
numpy 1.26.4
oauthlib 3.2.2
opencv-python-headless 4.9.0.80
openpyxl 3.1.2
outcome 1.3.0.post0
OWSLib 0.28.1
packaging 23.2
pandas 2.2.1
paramiko 3.4.0
parso 0.8.4
partd 1.4.1
pathspec 0.12.1
patsy 0.5.6
Pebble 5.0.7
pexpect 4.9.0
pickleshare 0.7.5
pillow 10.3.0
pip 24.0
pkgutil_resolve_name 1.3.10
platformdirs 4.2.0
plotly 5.19.0
plotnine 0.13.5
pluggy 1.4.0
polars 0.20.18
preshed 3.0.9
prometheus_client 0.20.0
prometheus-flask-exporter 0.23.0
prompt-toolkit 3.0.42
protobuf 4.25.3
psutil 5.9.8
ptyprocess 0.7.0
pure-eval 0.2.2
py7zr 0.20.8
pyarrow 15.0.0
pyarrow-hotfix 0.6
pyasn1 0.5.1
pyasn1-modules 0.3.0
pybcj 1.0.2
pycodestyle 2.11.1
pycosat 0.6.6
pycparser 2.21
pycryptodomex 3.20.0
pydantic 2.7.1
pydantic_core 2.18.2
pyflakes 3.2.0
Pygments 2.17.2
PyJWT 2.8.0
pylint 3.1.0
PyNaCl 1.5.0
pynsee 0.1.7
pyOpenSSL 24.0.0
pyparsing 3.1.2
pyppmd 1.1.0
pyproj 3.6.1
pyshp 2.3.1
PySocks 1.7.1
python-dateutil 2.9.0
python-dotenv 1.0.1
python-magic 0.4.27
pytz 2024.1
pyu2f 0.1.5
pywaffle 1.1.0
PyYAML 6.0.1
pyzmq 25.1.2
pyzstd 0.15.10
QtPy 2.4.1
querystring-parser 1.2.4
rasterio 1.3.10
referencing 0.34.0
regex 2023.12.25
requests 2.31.0
requests-cache 1.2.0
requests-oauthlib 2.0.0
rpds-py 0.18.0
rsa 4.9
Rtree 1.2.0
ruamel.yaml 0.18.6
ruamel.yaml.clib 0.2.8
s3fs 2023.12.2
s3transfer 0.10.1
scikit-image 0.23.2
scikit-learn 1.4.1.post1
scipy 1.13.0
seaborn 0.13.2
selenium 4.20.0
setuptools 69.2.0
shapely 2.0.3
six 1.16.0
smart-open 6.4.0
smmap 5.0.0
sniffio 1.3.1
snuggs 1.4.7
sortedcontainers 2.4.0
soupsieve 2.5
spacy 3.7.4
spacy-legacy 3.0.12
spacy-loggers 1.0.5
SQLAlchemy 2.0.29
sqlparse 0.4.4
srsly 2.4.8
stack-data 0.6.2
statsmodels 0.14.1
tabulate 0.9.0
tblib 3.0.0
tenacity 8.2.3
texttable 1.7.0
thinc 8.2.3
threadpoolctl 3.4.0
tifffile 2024.4.24
tomli 2.0.1
tomlkit 0.12.4
toolz 0.12.1
topojson 1.8
tornado 6.4
tqdm 4.66.2
traitlets 5.14.2
trio 0.25.0
trio-websocket 0.11.1
truststore 0.8.0
typer 0.9.4
typing_extensions 4.11.0
tzdata 2024.1
Unidecode 1.3.8
url-normalize 1.4.3
urllib3 1.26.18
wasabi 1.1.2
wcwidth 0.2.13
weasel 0.3.4
webcolors 1.13
webdriver-manager 4.0.1
websocket-client 1.7.0
Werkzeug 3.0.2
wheel 0.43.0
widgetsnbextension 4.0.10
wordcloud 1.9.3
wrapt 1.16.0
wsproto 1.2.0
xgboost 2.0.3
xlrd 2.0.1
xyzservices 2024.4.0
yarl 1.9.4
yellowbrick 1.5
zict 3.0.0
zipp 3.17.0
zstandard 0.22.0

View file history

SHA Date Author Description
06d003a 2024-04-23 10:09:22 Lino Galiana Continue la restructuration des sous-parties (#492)
005d89b 2023-12-20 17:23:04 Lino Galiana Finalise l’affichage des statistiques Git (#478)
3fba612 2023-12-17 18:16:42 Lino Galiana Remove some badges from python (#476)
4cd44f3 2023-12-11 17:37:50 Antoine Palazzolo Relecture NLP (#474)
a06a268 2023-11-23 18:23:28 Antoine Palazzolo 2ème relectures chapitres ML (#457)
889a71b 2023-11-10 11:40:51 Antoine Palazzolo Modification TP 3 (#443)
762f85a 2023-10-23 18:12:15 Lino Galiana Mise en forme du TP webscraping (#441)
8071bbb 2023-10-23 17:43:37 tomseimandi Make minor changes to 02b, 03, 04a (#440)
3eb0aeb 2023-10-23 11:59:24 Thomas Faria Relecture jusqu’aux API (#439)
102ce9f 2023-10-22 11:39:37 Thomas Faria Relecture Thomas, première partie (#438)
fbbf066 2023-10-16 14:57:03 Antoine Palazzolo Correction TP scraping (#435)
a771183 2023-10-09 11:27:45 Antoine Palazzolo Relecture TD2 par Antoine (#418)
154f09e 2023-09-26 14:59:11 Antoine Palazzolo Des typos corrigées par Antoine (#411)
b7f4d7e 2023-09-17 17:03:14 Antoine Palazzolo Renvoi vers sujet funathon pour partie scraping (#404)
9a4e226 2023-08-28 17:11:52 Lino Galiana Action to check URL still exist (#399)
8baf507 2023-08-28 11:09:30 Lino Galiana Lien mort formation webscraping
a8f90c2 2023-08-28 09:26:12 Lino Galiana Update featured paths (#396)
3bdf3b0 2023-08-25 11:23:02 Lino Galiana Simplification de la structure 🤓 (#393)
30823c4 2023-08-24 14:30:55 Lino Galiana Liens morts navbar (#392)
3560f1f 2023-07-21 17:04:56 Lino Galiana Build on smaller sized image (#384)
130ed71 2023-07-18 19:37:11 Lino Galiana Restructure les titres (#374)
ef28fef 2023-07-07 08:14:42 Lino Galiana Listing pour la première partie (#369)
f21a24d 2023-07-02 10:58:15 Lino Galiana Pipeline Quarto & Pages 🚀 (#365)
38693f6 2023-04-19 17:22:36 Lino Galiana Rebuild visualisation part (#357)
3248633 2023-02-18 13:11:52 Lino Galiana Shortcode rawhtml (#354)
3c880d5 2022-12-27 17:34:59 Lino Galiana Chapitre regex + Change les boites dans plusieurs chapitres (#339)
938f9bc 2022-12-04 15:28:37 Lino Galiana Test selenium en intégration continue (#331)
342b59b 2022-12-04 11:55:00 Romain Avouac Procedure to install selenium on ssp cloud (#330)
037842a 2022-11-22 17:52:25 Lino Galiana Webscraping exercice nom et age ministres (#326)
738c074 2022-11-17 12:23:29 Lino Galiana Nettoie le TP scraping (#323)
f5f0f9c 2022-11-02 19:19:07 Lino Galiana Relecture début partie modélisation KA (#318)
43a863f 2022-09-27 11:14:18 Lino Galiana Change notebook url (#283)
25046de 2022-09-26 18:08:19 Lino Galiana Rectifie bug TP webscraping (#281)
494a85a 2022-08-05 14:49:56 Lino Galiana Images featured ✨ (#252)
d201e3c 2022-08-03 15:50:34 Lino Galiana Pimp la homepage ✨ (#249)
bb38643 2022-06-08 16:59:40 Lino Galiana Répare bug leaflet (#234)
12965ba 2022-05-25 15:53:27 Lino Galiana :launch: Bascule vers quarto (#226)
9c71d6e 2022-03-08 10:34:26 Lino Galiana Plus d’éléments sur S3 (#218)
66e2837 2021-12-24 16:54:45 Lino Galiana Fix a few typos in the new pipeline tutorial (#208)
0e01c33 2021-11-10 12:09:22 Lino Galiana Relecture @antuki API+Webscraping + Git (#178)
9a3f7ad 2021-10-31 18:36:25 Lino Galiana Nettoyage partie API + Git (#170)
6777f03 2021-10-29 09:38:09 Lino Galiana Notebooks corrections (#171)
2a8809f 2021-10-27 12:05:34 Lino Galiana Simplification des hooks pour gagner en flexibilité et clarté (#166)
b138cf3 2021-10-21 18:05:59 Lino Galiana Mise à jour TP webscraping et API (#164)
2e4d586 2021-09-02 12:03:39 Lino Galiana Simplify badges generation (#130)
80877d2 2021-06-28 11:34:24 Lino Galiana Ajout d’un exercice de NLP à partir openfood database (#98)
4cdb759 2021-05-12 10:37:23 Lino Galiana :sparkles: :star2: Nouveau thème hugo :snake: :fire: (#105)
7f9f97b 2021-04-30 21:44:04 Lino Galiana 🐳 + 🐍 New workflow (docker 🐳) and new dataset for modelization (2020 🇺🇸 elections) (#99)
6d010fa 2020-09-29 18:45:34 Lino Galiana Simplifie l’arborescence du site, partie 1 (#57)
66f9f87 2020-09-24 19:23:04 Lino Galiana Introduction des figures générées par python dans le site (#52)
5c1e76d 2020-09-09 11:25:38 Lino Galiana Ajout des éléments webscraping, regex, API (#21)
Back to top

Citation

BibTeX citation:
@book{galiana2023,
  author = {Galiana, Lino},
  title = {Python Pour La Data Science},
  date = {2023},
  url = {https://pythonds.linogaliana.fr/},
  doi = {10.5281/zenodo.8229676},
  langid = {en}
}
For attribution, please cite this work as:
Galiana, Lino. 2023. Python Pour La Data Science. https://doi.org/10.5281/zenodo.8229676.