Comment ChatGPT a changé ma façon de scrapper internet et de courir mes trails

Robin Bonduelle
12 min readJul 24, 2024

--

J’ai toujours aimé le scrapping. Ce sentiment de pouvoir façonner son propre outil pour déjouer les limites rigides imposées par les développeurs d’applications. Moi qui n’ai jamais été délinquant pour un sou, je sentais grâce au scrapping le frisson de la zone grise…

Pêle-mêle, j’ai scrappé dans le passé les sites d’annonces immobilières pour Cozy Home, une application où je permettais à l’utilisateur de recevoir les résultats de toutes les plateformes en ne créant qu’une seule recherche. J’ai scrappé plusieurs fois des sites de concours de start ups, pour y voir les résultats en direct et générer des faux votes pour les copains. Ou encore ces fois où le scrapping m’a permis de manger gratuitement avec une application de livraison aujourd’hui disparue, en exploitant leur système de parrainage par des créations de comptes automatisées.

Aujourd’hui, je scrappe encore fréquemment en fonction des usages — growth hacking, curation de contenu, besoins personnels variés — avec peut-être un peu plus d’attention à ne plus profiter de cet avantage pour faire des choses somme toute répréhensibles.

Quoi qu’il en soit, le scrapping est un outil parmi d’autres à maîtriser pour parvenir à ses fins.

Là où je devais dans le passé écrire tous ces scripts à la main, en Python, je peux maintenant laisser ChatGPT générer ces scripts grâce à des prompts précis, sans écrire une seule ligne de code. Cela requiert une méthode précise, que je vais détailler ci-dessous, mais bien plus rapide que d’écrire ses propre scripts. Et surtout, c’est ouvert à tous le monde, pour peu qu’on ait quelques bases de ce qui se passe en coulisses et qu’on se laisse guider par ChatGPT. Alors oui, on peut aussi passer par les nombreuses plateformes no-code si la moindre vue d’un script donne le mal de mer, mais c’est comme tout, cela vient avec un brin de contrôle et de personnalisation en moins, ainsi qu’un coût supplémentaire.

Cet article a donc vocation à illustrer ces quelques étapes de prompt engineering, au travers d’un projet simple et récent : identifier mes concurrents en amont des courses trail auxquelles je participe.

Le projet

A chaque fois qu’une course arrive, c’est la même chose : je ne sais jamais qui vont être mes concurrents sur la ligne de départ 😱

Compliqué dans ces conditions d’optimiser sa stratégie de course et de faire de la prévisualisation mentale. Seul moyen à disposition pour les plus téméraires : parcourir la page d’inscription, et croiser çà et là dans cette masse interminable un nom qui nous dit quelque chose. On a tous mieux à faire…

En voici un exemple de cette liste interminable, qui comprend souvent quelques milliers de personnes :

Liste des participants à ma prochaine course

C’est donc exactement le genre de problème où le scrapping et ChatGPTG sont les bienvenus !

Leur mission pour cette fois, s’ils l’acceptent :

  • 👥 Récupérer l’ensemble des inscrits à la course sur le site de l’organisateur.
  • 💯 Pour chaque personne, rechercher son index UTMB sur le site de l’UTMB. L’index UTMB représente en gros le niveau du coureur concerné, un peu comme l’ELO aux échecs.
  • 📄 Créer une liste des personnes ayant un index >750, triée par ordre décroissant.
  • 😈 Se préparer à une rude bataille.

Et… Tadaaa!

Résultat de toutes ces manigances pour ma prochaine course

Quelque soit la course dans le futur, en quelques secondes, j’ai maintenant un script qui me donne accès à la liste précise de concurrents. Comme on le voit ci-dessous pour ma prochaine course, les Gabizos, on sera une petite dizaine à batailler devant. Enfin une petite dizaine derrière ce monstre de Jan Margarit Sole, qui jouera la gagne avec lui-même.

Alors, comment ChatGPT m’a t’il aidé pour créer ce projet en quelques dizaines de minutes ?

Quelques notions avant de commencer

Un brin de théorie tout d’abord.

Pour scrapper, le plus simple est d’utiliser un script Python, ce qui suppose (i) d’installer Python sur sa machine ainsi qu’un éditeur de code, et (ii) de savoir comment ouvrir son terminal.

Une fois cette douloureuse mise en route derrière nous, on peut invoquer 3 briques de base dans le scrapping :

  1. D’abord accéder à un site web et y récupérer le code HTML de la page souhaitée, ce que l’on va faire grâce à un module comme Beautiful Soup.
  2. Ensuite, retravailler ce code HTML pour en extraire les informations qui nous intéressent et les mettre en forme, ce que l’on va faire avec Beautiful Soup toujours, et du code Python.
  3. Enfin, si besoin est, cliquer sur des boutons et naviguer entre des pages, ce que l’on va faire en émulant un navigateur grâce à un module comme Selenium.

Bien sûr, le plus dur dans le scrapping n’est pas de récupérer et mettre en forme les informations contenue dans le HTML, mais précisément d’accéder à ce code HTML de manière programmatique. Il peut se cacher derrière un login / password. Il peut aussi avoir été protégé.

Il va alors s’agir de déjouer les pièges laissés par le développeur pour empêcher son contenu de se faire voler par le manant que l’on est. Captcha, bot detection, contrôle des IPs, zones logguées, etc, etc : il y a tout un arsenal d’embûches à surmonter pour parvenir à ses fins, qui vont réclamer un certain nombre de tâtonnements et de techniques plus ou moins avancées.

Heureusement, dans mon cas, aucune serrure à forcer : je suis probablement le seul allumé à scrapper des sites de trail. J’ai pu y entrer comme dans une église, comme c’est le cas dans la plupart des projets simples.

L’élaboration du script avec ChatGPT

C’est parti pour le prompting.

Pour mon projet, j’ai découpé le travail en deux taches principales. Il vaut toujours mieux procéder par étapes afin d’éviter que ChatGPT ne se retrouve avec une mission trop compliquée sur les bras et panique.

Deux taches intermédiaires, disais-je :

  1. Récupérer la liste des inscrits dans un tableau, sur le site d’inscriptions.
  2. Parcourir ce tableau, et pour chaque inscrit, faire une recherche d’index UTMB, sur le site de l’UTMB.

Parcourons ensemble les grandes lignes de réalisation de ce script.

Récupérer les inscrits dans un tableau, sur le site d’inscriptions

Ma première étape a été de poser une première brique en enlevant toute notion de complexité, à savoir récupérer les résultats souhaités sur la première page d’inscrits.

Voici le prompt que j’ai utilisé :

Can you write a python script that scraps the list of the results on this page using Beautiful Soup: <https://www.njuko.net/gabizostrail2024/registrations-list>?
The objective of this script is to collect the names of the persons that subscribed to the race.
# Instructions
- The script should collect the “Prénom” and “Nom” values in a concatenated variable “name”, for each person.
- It should only list people who attend the race “Gabizos Sky Race (30km / 2700m D+)” .
- It should then store the final output in a list like this: [name1, name2, name3, …].
- It eventually should print the final output.
- Add headers to simulate a real user connection.
# Example
- The list should look like this for the first page: [“Jean-pierre ABADIE”, “Philippe ABADIE”, “Perrine ABADIE”, …]

Quelques techniques utilisées ci-dessus :

  • Donner l’objectif globale du script de manière claire et concise.
  • Lister les instructions en étant le plus précis possible. Chaque once d’incertitude est un problème potentiel à corriger plus tard.
  • Donner l’exemple de résultat souhaité.

Grâce à ce prompt, j’ai obtenu une trame de script pour la première tache:

import requests
from bs4 import BeautifulSoup

# URL of the race registration list
base_url = 'https://www.njuko.net/gabizostrail2024/registrations-list'

# Set a user-agent to mimic a browser request
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

names = []

# Send a GET request to fetch the raw HTML content
response = requests.get(base_url, headers=headers)

# Parse the HTML content of the page
soup = BeautifulSoup(response.text, 'html.parser')

# Find the rows of the table that contains the participants' data
rows = soup.select('table tbody tr')

for row in rows:

# Check if the person is subscribed to the specified race
race_name = row.select_one('td:nth-child(3)').get_text(strip=True)
if race_name == "Gabizos Sky Race (30km / 2700m D+)":
# Extract the first name and last name
first_name = row.select_one('td:nth-child(2)').get_text(strip=True)
last_name = row.select_one('td:nth-child(1)').get_text(strip=True)
# Concatenate and add to the list
full_name = f"{first_name} {last_name}"
names.append(full_name)

# Print the collected names
print(names)

Par chance, ce script a marché du premier coup. C’est loin d’être toujours le cas. Dans le cas contraire, la méthode est toujours la même, et s’appuie sur deux principes :

  • Quand il y a une erreur, copier l’erreur dans son terminal et la transmettre à ChatGPT pour qu’il donne des indications et corrige le code.
  • Quand on est dans une impasse après quelques essais infructueux ou s’il n’y a pas d’erreur mais que le résultat n’est pas conforme à ce que l’on attend, mettre les mains dans le cambouis et regarder d’un peu plus près ce qui se passe. On peut par exemple insérer des “print” un peu partout dans le code, afin de voir ce que contient chaque variable au fur et à mesure du script. On peut aussi commenter des parties de code pour isoler plus précisément l’origine de l’erreur. On peut enfin utiliser Python pour repérer et afficher les erreurs proactivement, mais cela demandera une compréhension un peu plus fine du code.

J’étais déjà bien avancé avec un unique prompt, mais il fallait maintenant intégrer la pagination, chose un peu plus compliquée : l’itération entre les pages de résultats ne se faisant pas au niveau de l’URL grâce à un paramètre, il fallait donc cliquer sur le bloc de navigation. C’est là que Selenium entre en jeu. Pour aider ChatGPT à naviguer correctement dans le code HTML pour sa navigation, je lui ai transmis l’élément concerné, grâce à la console.

Copier le code HTML de l’élément sur lequel on veut agir

Voici le prompt utilisé :

OK thanks you. Now I need to iterate through the result pages, in order to collect all the users.
# Instructions
— Use Selenium to navigate across pages using the navigation component which is listed below in triple quotes.
— Insert header options and delays to simulate a real user navigation.
— Here is the button to navigate to page 2 for instance:
<a href=”/gabizostrail2024/registrations-list/page/2/items_per_page/100" title=”Page 2">2</a>

``` <div class=”pagination pagination-right” id=”pagination-nav”> <ul> <! — First page link → <li class=”first-page disabled”> <a href=”/gabizostrail2024/registrations-list/page/1/items_per_page/100" title=”Première page”>«</a></li> <! — Previous page link → <li class=”previous-page disabled”> <a href=”/gabizostrail2024/registrations-list/items_per_page/100" title=”page précédente”>‹</a> </li> <! — Numbered page links → <li class=”page page-1 active”> <a href=”/gabizostrail2024/registrations-list/page/1/items_per_page/100" title=”Page 1"> 1 </a> </li> <li class=”page page-2 “> <a href=”/gabizostrail2024/registrations-list/page/2/items_per_page/100" title=”Page 2"> 2 </a> </li> <li class=”page page-3 “> <a href=”/gabizostrail2024/registrations-list/page/3/items_per_page/100" title=”Page 3"> 3 </a> </li> <li class=”page page-4 “> <a href=”/gabizostrail2024/registrations-list/page/4/items_per_page/100" title=”Page 4"> 4 </a> </li> <li class=”page page-5 “> <a href=”/gabizostrail2024/registrations-list/page/5/items_per_page/100" title=”Page 5"> 5 </a> </li> <! — Next page link → <li class=”next-page “> <a href=”/gabizostrail2024/registrations-list/page/2/items_per_page/100" title=”Page suivante”>›</a></li> <! — Last page link → <li class=”last-page “> <a href=”/gabizostrail2024/registrations-list/page/11/items_per_page/100" title=”Dernière page”>»</a></li> </ul> </div> ```

Cette fois-ci, le script n’a pas marché du premier coup, et m’a demandé un peu de debugging avant d’arriver au résultat souhaité : un tableau contenant tous les noms et prénoms des personnes inscrites à la course.

Rechercher l’index UTMB de chaque inscrit, sur le site de l’UTMB

La page de recherche de l’UTMB

Pour cette seconde étape, il y avait deux options. Soit compléter le script initial, soit créer un nouveau script, auquel on fournit le tableau récupéré en amont. La seconde option garde les choses plus simples pour ChatGPT, donc elle est surement à privilégier. A noter que je n’ai pas suivi cette direction, mais que j’ai encapsulé le travail du script précédent dans des fonctions pour isoler le code malgré tout.

Je ne vais pas détailler ici l’ensemble des prompts car le principe est toujours le même, et je me suis laissé guider par ChatGPT chemin faisant. Quelques points d’attention qu’il a fallu traiter en cours de route cependant :

  • Le bouton recherche sur lequel cliquer pour activer la recherche n’était pas clair du tout, puisqu’il s’agissait d’un SVG. J’ai du précisément l’indiquer à ChatGPT en lui copiant l’élément HTML concerné et en lui indiquant qu’il fallait cliquer sur ce SVG.
  • La page mettant du temps à charger la recherche, j’ai du ajouter des délais et conditions afin de bien récupérer les valeurs une fois la page chargée.
  • La recherche remontait des résultats même quand le coureur n’avait pas d’index UTMB, il a donc fallu vérifier que le coureur inscrit et le résultat de la recherche soient bien similaires, avec toutes les précautions requises (mettre du lowercase des deux côtés, comparer dans le sens Nom Prénom et Prénom Non, etc …)

Au bout du compte, voici le script final obtenu :

import time
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from selenium.webdriver.chrome.options import Options

# Initialize the Chrome options to run in headless mode
chrome_options = Options()
#chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")

# Initialize the WebDriver with the headless option
driver = webdriver.Chrome(options=chrome_options)
wait = WebDriverWait(driver, 10) # Initialize WebDriverWait

def get_soup(driver):
return BeautifulSoup(driver.page_source, 'html.parser')

def extract_names(soup, competition_filter):
names = []
rows = soup.select('table tbody tr')
for row in rows:
competition = row.select_one('td:nth-child(3)').get_text(strip=True)
if competition == competition_filter:
first_name = row.select_one('td:nth-child(2)').get_text(strip=True)
last_name = row.select_one('td:nth-child(1)').get_text(strip=True)
names.append(f"{first_name} {last_name}")
print(len(names))
return names

def click_next_page(driver):
try:
next_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#pagination-nav .next-page')))
if 'disabled' in next_button.get_attribute('class'):
return False
next_link = next_button.find_element(By.TAG_NAME, 'a')
if next_link:
next_link.click()
return True
return False
except Exception as e:
print(f"No more pages or error occurred: {e}")
return False

def set_results_per_page(driver):
try:
select_element = Select(wait.until(EC.presence_of_element_located((By.ID, "items_per_page"))))
select_element.select_by_value('100')
time.sleep(2) # Wait for the page to reload
except Exception as e:
print(f"Error setting results per page: {e}")

def search_utmb_index(name):

search_query = name

try:
# Wait for the search input to be present and find it
search_input = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "index-ranking-table_input__ZIkBo"))
)
search_input.clear()
search_input.send_keys(search_query)

# Add a 2-second delay after filling the input
time.sleep(3)

# Find the parent container of the SVG search button and click it
search_button_container = driver.find_element(By.CLASS_NAME, "index-ranking-table_search_container__sTB8q")
driver.execute_script("arguments[0].click();", search_button_container)

# Wait for the results to load and find the first result row
first_result_row = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "my-table_row__nlm_j"))
)

# Extract the username from the row
username_element = first_result_row.find_element(By.CLASS_NAME, "link_link__96ppl")
username = username_element.text.strip()

# Check if the extracted username matches the provided name
if username.lower() == name.lower():
# Extract the UTMB index from the first cell in the row
utmb_index_cell = first_result_row.find_element(By.CLASS_NAME, "my-table_cell__z__zN")
utmb_index = utmb_index_cell.text.strip()
return utmb_index
else:
return f"Name {name} does not match username {username} in the table."

except Exception as e:
return f"An error occurred: {e}"

def get_utmb_indices(names):
utmb_indices = {}
for name in names:
utmb_index = search_utmb_index(name)
if utmb_index.isdigit() and int(utmb_index) > 750:
utmb_indices[name] = utmb_index
print(f"{name} : {utmb_index}")
sorted_utmb_indices = dict(sorted(utmb_indices.items(), key=lambda item: int(item[1]) if item[1].isdigit() else -1, reverse=True))
return sorted_utmb_indices

def main():
competition_url = "https://www.njuko.net/gabizostrail2024/registrations-list"
competition_filter = "Gabizos Sky Race (30km / 2700m D+)"
driver.get(competition_url)
# Set results per page to 100
set_results_per_page(driver)
all_names = []

while True:
soup = get_soup(driver)
all_names.extend(extract_names(soup, competition_filter))
if not click_next_page(driver):
break
time.sleep(2) # Wait for the next page to load

print(all_names)
print(len(all_names))

utmb_url = "https://utmb.world/fr/utmb-index/runner-search"
driver.get(utmb_url)

utmb_indices = get_utmb_indices(all_names)
print(utmb_indices)


if __name__ == "__main__":
main()

Je peux maintenant le réutiliser pour mes prochaines courses, en changeant simplement l’URL de l’épreuve, ainsi que le nom de la course.

Quelques mots pour conclure

Ce script n’est pas le plus propre qui soit, même si à vrai dire, il y a de larges portions que je n’ai même pas lu. J’aurais surement écrit quelque chose d’un peu plus évolué par moi-même (encore que), mais au prix d’efforts supplémentaires, ne codant pas suffisamment souvent pour garder tous les automatismes d’une fois sur l’autre.

Surtout, il marche, et c’est ce qu’on lui demande pour des usages simples qui ne requièrent que peu ou pas d’automatisation, dans un contexte “hors production”.

Le revers de la médaille, peut-être, provient bien du fait que je deviens de plus en plus passif, à demander son avis à ChatGPT pour un oui ou pour un non dès que j’ai un problème sur mon script, sans même m’essayer à corriger les choses par moi-même en première intention. On retrouve le bon syndrome du calcul mental devenu complètement superflu à l’heure des calculettes. Et on en arrive au constat troublant que ma productivité augmente quand mon expertise baisse au fil du temps…

--

--