Cours

Vous trouverez un exemple complet et fonctionnel du script que nous allons écrire ici.
Qu'est-ce que le web scraping ?
Très bien, mais qu’est-ce que le web scraping au juste ? Comme son nom l’indique, c’est une méthode qui consiste à « gratter » ou extraire des données de pages web. Tout ce que vous pouvez voir sur internet avec votre navigateur, y compris ce tutoriel, peut être aspiré et enregistré sur votre disque dur.
Le web scraping a de nombreux usages. Pour toute analyse de données, la première étape est l’acquisition des données. Internet est un immense réservoir de l’histoire et des connaissances de l’humanité, et vous avez les moyens d’en extraire ce dont vous avez besoin pour exploiter ces informations.
Dans ce tutoriel, nous utiliserons Python et le package BeautifulSoup 4 pour récupérer des informations depuis un subreddit. Nous nous intéressons au subreddit datascience. Nous voulons récupérer les 1000 premiers posts du subreddit et les exporter dans un fichier CSV. Nous souhaitons savoir qui les a publiés, ainsi que le nombre de likes et de commentaires.
Ce que nous allons couvrir dans ce tutoriel :
- Récupérer des pages web avec
requests - Analyser des pages web dans le navigateur pour identifier les informations
- Extraire des informations du HTML brut avec BeautifulSoup
Remarque : nous utiliserons l’ancienne version du site de Reddit, plus légère à charger et donc moins exigeante pour votre machine.
Prérequis
Ce tutoriel suppose que vous savez :
- Exécuter des scripts Python sur votre ordinateur
- Les bases de la structure HTML
Vous pouvez acquérir ces compétences dans le cours d’introduction à Python de DataCamp. Cela dit, les concepts utilisés ici sont très simples, et une connaissance minimale de Python suffit.
Maintenant que c’est fait, passons à la première étape de notre scraper web. En fait, la première étape de tout script Python : les imports.
Dans notre scraper, nous utiliserons les packages suivants :
requestsbeautifulsoup4
Vous pouvez bien sûr installer ces packages avec pip, comme ceci :
pip install package_name
Une fois les packages téléchargés, importez-les dans votre code.
import requests
import csv
import time
from bs4 import BeautifulSoup4
Nous utiliserons le module intégré csv de Python pour écrire nos résultats dans un fichier CSV. Vous avez peut-être remarqué quelque chose d’étrange dans l’extrait ci-dessus. Nous avons installé un package appelé beautifulsoup4, mais nous avons importé depuis un module nommé bs4. C’est autorisé en Python : même si cela fait parfois grincer des dents, ce n’est pas interdit.
Note importante sur les politiques des plateformes
Avant d’utiliser les techniques de scraping présentées ci-dessous, vérifiez les conditions d’utilisation en vigueur de Reddit ou de tout autre site que vous envisagez d’extraire. Les politiques des sites peuvent évoluer. L’accord utilisateur de Reddit de septembre 2024 interdit le scraping sans consentement écrit préalable (voir la section 7). Assurez-vous toujours de respecter les dernières directives d’utilisation du site avant de procéder.
Premiers pas pour explorer Reddit
Notre environnement est prêt. Il nous faut maintenant l’URL de la page web à scraper. Pour ce tutoriel, nous utilisons le subreddit « datascience ». Avant d’écrire le script, un peu de repérage s’impose. Ouvrez un navigateur et rendez-vous sur le subreddit en question.
Notez que nous utiliserons l’ancienne version du subreddit pour notre scraper. La nouvelle version masque certaines informations cruciales dans les entrailles de la page. Il est possible d’extraire ces informations depuis le nouveau site également, mais pour simplifier, nous optons pour l’ancienne version qui expose tout clairement.
En ouvrant le lien, vous êtes inondé d’informations. Que nous faut-il exactement dans tout cela ? En examinant les liens, vous constaterez que les posts Reddit sont de deux types :
- Entrants
- Sortants
Les liens entrants pointent vers du contenu hébergé sur Reddit lui‑même, tandis que les sortants font l’inverse. C’est important, car nous ne voulons que les liens entrants : des posts contenant du texte rédigé par les utilisateurs, pas de simples liens vers d’autres sites.
Très bien, nous savons ce que nous voulons. Comment l’extraire ? Si vous regardez le titre des posts, vous verrez qu’il est suivi d’un texte entre crochets. Les posts qui nous intéressent sont suivis de « (self.datascience) ». Logiquement, « self » fait référence à la racine de Reddit, et « .datascience » au subreddit.
Parfait, nous avons une manière d’identifier les posts entrants par opposition aux sortants. Reste à les repérer dans le DOM. Peut‑on retrouver ces identifiants dans le DOM en cherchant des balises, classes ou IDs ? Bien sûr !
Dans votre navigateur, faites un clic droit sur un lien « self.datascience » puis « Inspecter l’élément ». La plupart des navigateurs modernes disposent de cet outil qui permet d’explorer dynamiquement les parties pertinentes du code source de la page. Sur Safari, veillez à activer les outils de développement dans les préférences. Un panneau s’ouvrira, pointant sur la portion de code à l’origine de l’identifiant « (self.datascience) ».
Dans ce panneau, vous voyez que l’identifiant est en réalité chargé par une balise ancre.
<a href="/r/datascience">self.datascience</a>
Mais celle‑ci est encapsulée dans une balise span.
<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>
Ce span contient le texte affiché sur la page, c’est‑à‑dire « (self.datascience) ». Comme vous le voyez, il porte la classe « domain ». Vous pouvez vérifier les autres liens : ils suivent le même format.
Récupérer la page avec BeautifulSoup
Nous savons ce que nous voulons sur la page, très bien, mais comment utiliser Python pour lire son contenu ? Le principe est similaire à la lecture par un humain via un navigateur.
Commençons par demander la page web avec la bibliothèque « requests ».
url = "https://old.reddit.com/r/datascience"
# En-têtes pour imiter une visite de navigateur
headers = {'User-Agent': 'Mozilla/5.0'}
# Retourne un objet requests.models.Response
page = requests.get(url, headers=headers)
Nous avons maintenant un objet Response qui contient le texte brut de la page. Pour l’instant, nous ne pouvons rien en faire. Ce n’est qu’une longue chaîne qui contient tout le code source HTML. Pour y voir clair, nous devons utiliser BeautifulSoup 4.
Les en‑têtes nous permettent d’imiter une visite depuis un navigateur. La réponse envoyée à un bot peut différer de celle envoyée à un navigateur, et comme notre référence est le navigateur, mieux vaut récupérer cette réponse‑là.
BeautifulSoup nous permet de trouver des balises spécifiques en cherchant toute combinaison de classes, d’IDs ou de noms de balises. Il crée pour cela un arbre syntaxique, mais les détails importent peu ici (et sortent du cadre de ce tutoriel).
Créons donc cet arbre.
soup = BeautifulSoup(page.text, 'html.parser')
« soup » est un objet BeautifulSoup créé à partir du code source brut. N’oubliez pas de préciser le parseur HTML, car BeautifulSoup peut aussi parser du XML.
Trouver nos balises
Nous savons quelles balises nous voulons (les spans avec la classe « domain ») et nous avons la soupe. Il s’agit maintenant de la parcourir pour trouver toutes les occurrences de ces balises. Vous allez rire tant c’est simple avec BeautifulSoup.
domains = soup.find_all("span", class_="domain")
Que venons‑nous de faire ? Nous avons appelé la méthode « find_all » sur l’objet soup, qui recherche en lui‑même toutes les balises correspondant aux paramètres fournis en second argument. Nous n’avons passé qu’une contrainte (la classe doit être « domain »), mais on pourrait en ajouter d’autres, voire utiliser un id.
Nous utilisons « class_= » parce que « class » est un mot‑clé réservé en Python pour définir des classes, etc.
Si vous souhaitez passer plusieurs paramètres, il suffit de fournir en second argument un dictionnaire des attributs souhaités, comme ceci :
soup.find_all("span", {"class": "domain", "height", "100px"})
Nous avons tous les spans dans notre liste « domains », mais nous ne voulons que ceux de « (self.datascience) ».
for domain in domains:
if domain != "(self.datascience)":
continue
print(domain.text)
Vous devriez maintenant voir la liste des types de posts de la page, à l’exclusion de ceux qui ne sont pas « (self.datascience) ». Nous avons donc tout ce qu’il nous faut : des références à tous les posts entrants.
Trouver nos informations
Imprimer quelques lignes « (self.datascience) », c’est bien, mais ensuite ? Rappelons notre objectif initial : récupérer le titre du post, l’auteur, les likes et le nombre de commentaires.
Pour cela, retour au navigateur. En ouvrant l’inspecteur sur notre identifiant, vous verrez que notre span est imbriqué dans une div… elle‑même imbriquée dans une autre, et ainsi de suite. En remontant, vous atteignez la div parente du post entier.
<div class=" thing id-t3_8qccuv even link self" ...>
...
<div class="entry unvoted">
<div class="top-matter">
<p class="title">
...
<span class="domain">
...
</p>
</div>
</div>
</div>
Comme vous le voyez, le parent commun se trouve quatre niveaux au‑dessus dans la structure du DOM. Les points de suspension (…) représentent ce qui n’est pas utile ici. Nous avons maintenant besoin d’une référence à la div du post pour chaque élément de notre liste « domains ». On peut retrouver le parent de n’importe quel élément de la soupe via les méthodes de BeautifulSoup.
for domain in soup.find_all("span", class_="domain"):
if domain != "(self.datascience)":
continue
parent_div = domain.parent.parent.parent.parent
print(parent_div.text)
Exécuter ce script imprimera tout le texte de tous les posts entrants. Vous trouvez ce code fragile ? Vous avez raison. Il n’est jamais sûr de s’appuyer uniquement sur l’intégrité structurelle du DOM. C’est une solution « bricolage » qui nécessite des mises à jour constantes et ressemble à une bombe à retardement.
À la place, examinons la div parente de chaque post pour voir si nous pouvons distinguer liens entrants et sortants dès ce niveau. Chaque div parente possède un attribut « data-domain », dont la valeur est précisément ce qu’il nous faut ! Tous les posts entrants ont « data-domain » défini à « self.datascience ».
Comme mentionné, nous pouvons rechercher des balises par combinaison d’attributs avec BeautifulSoup. Coup de chance, Reddit a classé la div parente de chaque post avec « thing ».
attrs = {'class': 'thing', 'data-domain': 'self.datascience'}
for post in soup.find_all('div', attrs=attrs):
print(post.attrs['data-domain'])
La variable « attrs » est un dictionnaire décrivant les attributs et valeurs à rechercher. Cette approche est bien plus sûre car elle réduit les points de défaillance. Puisque nous filtrons déjà sur « self.datascience », plus besoin de l’instruction if pour ignorer des itérations : nous ne récupérerons que des posts dont l’attribut « data-domain » vaut « self.datascience ».
Nous avons désormais ce qu’il nous faut : il ne reste qu’à extraire l’information depuis les enfants. Et c’est aussi simple que vous l’imaginez.
Extraire nos informations depuis Reddit
Pour chaque post, il nous faut 4 éléments :
- Titre
- Auteur
- Likes
- Commentaires
En examinant la structure de la div du post, nous pouvons tout retrouver.
Le titre
C’est le plus simple. Dans chaque div de post, on trouve un paragraphe imbriqué sous quelques couches de div, facilement repérable grâce à la classe « title ».
title = post.find('p', class_="title").text
L’objet « post » est issu de notre boucle précédente. C’est aussi un objet BeautifulSoup, mais limité au DOM de la div du post. La méthode « find » ne retourne qu’un seul objet, contrairement à « find_all » qui renvoie une liste. Après avoir trouvé la balise, nous ne voulons que la chaîne du titre : nous utilisons donc sa propriété « text ». On peut aussi lire d’autres attributs via « objet.attrs[‘attribut’] ».
L’auteur
Toujours simple : ouvrez l’inspecteur sur le nom d’un auteur sous le titre. Vous verrez que le nom est dans une balise ancre avec la classe « author ».
author = post.find('a', class_='author').text
Les commentaires
Cela demande un petit traitement en plus, mais reste simple. Nous trouvons les commentaires comme pour le titre et l’auteur.
comments = post.find('a', class_='comments').text
À l’exécution, vous obtiendrez quelque chose comme « 49 comments ». C’est acceptable, mais mieux vaut ne récupérer que le nombre. Pour ce faire, un peu de Python.
En utilisant la fonction « str.split() », on obtient un tableau des éléments séparés par des espaces. Ici, la liste « ["49", "comments"] ». Parfait ! Il suffit de prendre le premier élément et de le stocker. Ajoutons donc ces fonctions à notre ligne.
comments = post.find('a', class_='comments').text.split()[0]
Mais ce n’est pas tout : parfois, on ne récupère pas un nombre, mais « comment ». Cela arrive lorsqu’un post n’a aucun commentaire. Puisque nous le savons, testons ce cas et remplaçons‑le par « 0 ».
if comments == "comment":
comments = 0
Les likes
Récupérer le nombre de likes est un jeu d’enfant, selon la même logique.
likes = post.find("div", attrs={"class": "score likes"}).text
Ici, vous remarquerez l’usage d’une combinaison de deux classes. C’est parce qu’il existe plusieurs div avec « score » ou « likes », mais une seule avec la combinaison « score likes ».
Si le nombre de likes est 0, nous obtenons 0. Mais si le post est tout nouveau et n’a pas encore de likes, on récupère « • ». Remplaçons‑le par « None » pour éviter toute confusion dans nos résultats.
if likes == "•":
likes = "None"
Écrire nos résultats en CSV
Jusqu’ici, nous avons une boucle qui extrait le titre, l’auteur, les likes et les commentaires pour chaque post de la page. Python propose un excellent module intégré pour lire et écrire des fichiers CSV, nommé « csv ». Il suffit d’ajouter en fin de bloc de boucle une ligne qui ajoute les informations du post à un fichier CSV.
counter = 1
for post in posts:
...
post_line = [counter, title, author, likes, comments]
with open('output.csv', 'a') as f:
writer = csv.writer(f)
writer.writerow(post_line)
counter += 1
Assez direct, non ? « post_line » est un tableau des éléments à séparer par des virgules. La variable « counter » sert à compter combien de posts ont été enregistrés.
Passer à la page suivante
Puisque nous comptons les posts enregistrés, il suffit d’englober toute notre logique dans une autre boucle qui demande de nouvelles pages jusqu’à atteindre un certain nombre de posts.
counter = 1
while (counter <= 100):
for post in posts:
# Récupération du titre, de l'auteur, ...
# Écriture dans le CSV et incrément du compteur...
next_button = soup.find("span", class_="next-button")
next_page_link = next_button.find("a").attrs['href']
time.sleep(2)
page = requests.get(next_page_link, headers=headers)
soup = BeautifulSoup(page.text, 'html.parser')
Nous venons de faire plusieurs choses. Voyons‑les ligne par ligne. D’abord, nous avons remonté la variable « counter » d’un bloc pour qu’elle soit accessible à la boucle while.
Ensuite, nous savons ce que fait la boucle for, mais qu’en est‑il du reste ? La variable « next_button » sert à trouver et stocker le bouton « next ». Puis, nous trouvons la balise ancre à l’intérieur et récupérons l’attribut « href », stocké dans « next_page_link ».
Nous utilisons ce lien pour demander la page suivante, l’enregistrons dans « page » et créons une nouvelle soupe avec BeautifulSoup.
L’extrait ci‑dessus s’arrête après 100, mais vous pouvez choisir n’importe quel seuil.
Scraper de manière responsable
La ligne dont nous n’avons pas parlé ci‑dessus est « time.sleep(2) ». Elle mérite sa propre section. Tout serveur web dispose de ressources limitées : à nous de veiller à ne pas les saturer. Non seulement envoyer des centaines de requêtes en quelques secondes peut vous faire bannir, mais ce n’est pas correct.
Gardez à l’esprit que les sites vous rendent déjà service en tolérant le scraping. S’ils le souhaitaient, ils pourraient détecter un bot en 10 à 20 requêtes, voire vous identifier via l’objet de requête envoyé par Python. Puisqu’ils vous évitent d’avoir à faire tourner des IP, rendez‑leur la pareille en ralentissant votre bot.
Vous pouvez consulter la politique d’exploration d’un site via son fichier robots.txt, généralement à la racine (ex. : http://www.reddit.com/robots.txt).
Et ensuite ?
Ce que vous voulez. Nous venons de montrer que tout ce que vous voyez sur le web peut être extrait et stocké localement. Les informations récupérées peuvent servir à de multiples usages. Avec seulement ces quatre éléments, on peut déjà tirer bien des enseignements.
En analysant plus finement nos données, on peut déterminer quels types de posts Reddit reçoivent le plus de likes, quels mots ils contiennent, qui les publie. À l’inverse, on peut concevoir un « calculateur de controverse » en analysant le ratio likes/commentaires.
Imaginez un post avec beaucoup de commentaires mais peu de likes. Il suscite sans doute une forte réaction, mais pas forcément positive.
Les possibilités sont nombreuses : à vous de décider comment exploiter ces informations.
Si ce tutoriel vous a plu, consultez aussi notre guide sur le scraping d’Amazon avec Python, ainsi que notre sélection de cours Python.