Extraction de donnĂ©es web 🔍

Extraction de donnĂ©es web 🔍

Internet est une source d’information incroyable. Mais les donnĂ©es ne sont pas toujours facilement tĂ©lĂ©chargeables. Parfois, l’information est simplement affichĂ©e sur des pages web et
 c’est tout !

Pour la récupérer, vous devez extraire les données directement à partir du code HTML et, sur des sites plus complexes, vous devez automatiser ou simuler des clics sur la page pour obtenir ce que vous voulez.

Un mot d’avertissement : avant toute extraction, assurez-vous toujours que ce que vous faites est lĂ©gal. Dans certains cas, l’extraction et la copie de donnĂ©es sont interdites. De plus, respectez toujours l’infrastructure. N’inondez pas les sites web de requĂȘtes. Soyez conscient des ressources et des coĂ»ts que votre scraping peut engendrer pour les personnes et les organisations qui hĂ©bergent ces sites.

Notez que je pars du principe que vous avez complĂ©tĂ© les sessions prĂ©cĂ©dentes de ce cours. La section 4. Fondamentaux du web 🌐 est particuliĂšrement importante. Si vous ne savez pas comment une page web est construite, il vous sera trĂšs difficile d’en extraire des donnĂ©es.

Vous voulez ĂȘtre prĂ©venu quand de nouvelles leçons sont publiĂ©es ? Abonnez-vous Ă  l'infolettre ✉ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Cliquez ici pour me contacter.

Configuration

Pour configurer notre projet, utilisons setup-sda, comme nous l’avons fait dans les leçons prĂ©cĂ©dentes, mais avec l’option --scrape pour installer quelques librairies supplĂ©mentaires.

Créez un nouveau dossier, ouvrez-le avec VS Code et exécutez deno -A jsr:@nshiab/setup-sda --scrape.

Une capture d'écran de VS Code montrant un script simulant la bourse

Extraction de tableaux

Les tableaux HTML sont communs pour afficher des données sur les sites web.

Par exemple, sur cette page Wikipédia sur la démographie médiévale en Europe, vous pouvez voir plusieurs tableaux.

Si vous inspectez celui nommĂ© European population dynamics, years 1000–1500 dans un navigateur (clic droit sur le tableau puis Inspecter dans le menu qui s’ouvre), vous verrez l’élĂ©ment HTML du tableau. Si vous explorez son code, vous verrez les diffĂ©rents Ă©lĂ©ments qui composent ce tableau avec les donnĂ©es qu’il contient.

Inspection d’une page HTML avec Chrome.

Comme ces structures HTML sont toujours les mĂȘmes, j’ai publiĂ© une fonction pour extraire les donnĂ©es intĂ©grĂ©es dans des tableaux comme celui-ci dans la librairie journalism. La librairie est installĂ©e automatiquement lorsque vous installez tout avec setup-sda.

Avec un index

Vous pouvez passer Ă  getHtmlTable l’URL que vous souhaitez utiliser. Par dĂ©faut, elle renverra les donnĂ©es du premier tableau de la page. Mais sur la page WikipĂ©dia, le tableau que nous voulons est en rĂ©alitĂ© le quatriĂšme dans le code HTML. Nous pouvons donc passer l’option { index: 3 }.

Copiez-collez le code ci-dessous dans sda/main.ts et exĂ©cutez deno task sda dans votre terminal pour lancer et surveiller l’exĂ©cution.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { getHtmlTable } from "@nshiab/journalism";
 
const sdb = new SimpleDB();
 
const medievalData = await getHtmlTable(
  "https://en.wikipedia.org/wiki/Medieval_demography",
  { index: 3 },
);
 
console.table(medievalData);
 
await sdb.done();

Un tableau Wikipédia extrait.

💡

Si la mise en page du tableau s’affiche Ă©trangement dans votre terminal, c’est parce que la largeur du tableau dĂ©passe celle du terminal. Faites un clic droit dans le terminal et cherchez l’option Toggle size with content width. Il existe aussi un raccourci trĂšs pratique que j’utilise tout le temps pour ça : OPTION + Z sur Mac et ALT + Z sur PC.

Les donnĂ©es retournĂ©es par getHtmlTable sont une liste d’objets, ce qui signifie que vous pouvez facilement les utiliser avec la librairie Simple Data Analysis pour les mettre en cache et les analyser.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { getHtmlTable } from "@nshiab/journalism";
 
const sdb = new SimpleDB();
 
const medievalDemography = sdb.newTable("mediavalDemography");
 
await medievalDemography.cache(async () => {
  const medievalData = await getHtmlTable(
    "https://en.wikipedia.org/wiki/Medieval_demography",
    { index: 3 },
  );
  await medievalDemography.loadArray(medievalData);
});
 
await medievalDemography.convert({
  Year: "number",
  "Total European population,millions": "number",
});
await medievalDemography.logTable();
await medievalDemography.logLineChart(
  "Year",
  "Total European population,millions",
);
 
await sdb.done();

Un tableau Wikipédia mis en cache.

💡

Mettre les donnĂ©es en cache est trĂšs important. Si vous ne vous attendez pas Ă  ce que les donnĂ©es changent, vous pouvez les sauvegarder sur votre ordinateur au lieu de les rĂ©cupĂ©rer encore et encore. Avec la librairie SDA, le cache est trĂšs simple Ă  gĂ©rer. La mĂ©thode cache crĂ©e un dossier .sda-cache qui stocke les donnĂ©es dans votre projet. Si vous souhaitez que les donnĂ©es expirent aprĂšs un certain temps, consultez l’option ttl dans la documentation. Pour en savoir plus sur la librairie SDA, consultez la section 3. La librairie SDA đŸ€“.

Avec un selector

La fonction getHtmlTable peut aussi utiliser un sĂ©lecteur CSS pour rĂ©cupĂ©rer les donnĂ©es d’un tableau spĂ©cifique que vous souhaitez cibler.

Par exemple, les dĂ©putĂ©s canadiens doivent divulguer leurs dĂ©penses tous les trois mois. Sur cette page, on peut voir que les donnĂ©es sont stockĂ©es dans un tableau avec l’identifiant data-table. On peut utiliser cet identifiant directement avec notre fonction.

PS : Notez que vous pouvez télécharger les données directement au format CSV. Mais je cherchais un site public qui ne change pas trop avec le temps, et celui-ci correspond bien à ce critÚre !

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { getHtmlTable } from "@nshiab/journalism";
 
const sdb = new SimpleDB();
 
const expensesData = await getHtmlTable(
  "https://www.ourcommons.ca/proactivedisclosure/en/members/2024/1",
  { selector: "#data-table" },
);
 
console.table(expensesData);
 
await sdb.done();

DĂ©penses des dĂ©putĂ©s extraites d’un tableau web.

Comme nous l’avons fait avec le tableau WikipĂ©dia, nous pouvons mettre en cache et analyser ces donnĂ©es avec SDA. Ici, nous calculons les dĂ©penses moyennes par parti.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { getHtmlTable } from "@nshiab/journalism";
 
const sdb = new SimpleDB();
 
const MPs = sdb.newTable("MPs");
await MPs.cache(async () => {
  const expensesData = await getHtmlTable(
    "https://www.ourcommons.ca/proactivedisclosure/en/members/2024/1",
    { selector: "#data-table" },
  );
  await MPs.loadArray(expensesData);
});
 
await MPs.replace(["Salaries", "Travel", "Hospitality", "Contracts"], {
  "$": "",
  ",": "",
});
await MPs.convert({
  Salaries: "number",
  Travel: "number",
  Hospitality: "number",
  Contracts: "number",
}, { try: true });
await MPs.summarize({
  values: ["Salaries", "Travel", "Hospitality", "Contracts"],
  categories: "Caucus",
  summaries: "mean",
  decimals: 0,
});
await MPs.wider("Caucus", "mean");
await MPs.logTable();
 
await sdb.done();

Dépenses des députés extraites et résumées avec SDA.

Extraction depuis des pages

Pages simples

Les données ne sont pas toujours bien rangées dans des tableaux. Parfois, elles traßnent un peu partout dans le code des pages web.

Par exemple, voici la liste de tous les dĂ©putĂ©s canadiens actuellement en fonction. Cette liste change avec le temps, donc vous n’aurez peut-ĂȘtre pas exactement les mĂȘmes que moi, mais ce n’est pas grave pour cette leçon.

Tous les députés actuellement en fonction.

Si vous voulez connaütre leur langue d’usage (le Canada est bilingue 🇹🇩), vous devez cliquer sur la page personnelle de chacun d’eux.

Comment pourrait-on récupérer la langue préférée de tous les députés ?

Page personnelle d’un dĂ©putĂ©.

Quand les données sont réparties sur plusieurs pages, la premiÚre étape consiste souvent à rassembler toutes les URLs.

Pour rĂ©cupĂ©rer toutes les URLs, on peut utiliser Playwright. C’est un projet en code ouvert de Microsoft. Il a Ă©tĂ© créé pour automatiser les tests de sites web, mais c’est aussi un excellent outil pour l’extraction de donnĂ©es. Playwright permet de prendre le contrĂŽle d’un navigateur web avec du code et d’extraire ce que vous voulez des pages visitĂ©es par votre script.

Playwright est installé automatiquement quand vous installez tout avec setup-sda.

Si on inspecte les cartes sur le site web, on peut voir que la balise a contient le lien vers la page personnelle du député. Toutes les balises ont la classe ce-mip-mp-tile.

Inspection de la page personnelle d’un dĂ©putĂ©.

Commençons par visiter la page et extraire les URLs. Voici ce que fait le code ci-dessous, étape par étape :

  • D’abord, on importe le navigateur chromium depuis Playwright. Chromium est un projet en code ouvert Ă  la base de Google Chrome. On crĂ©e un nouveau browser, puis un nouveau context, et enfin une newPage (lignes 1–5).
  • Ensuite, on demande Ă  cette page d’aller sur la page des dĂ©putĂ©s (ligne 7).
  • On rĂ©cupĂšre tous les Ă©lĂ©ments avec la classe ce-mip-mp-tile (ligne 9).
  • Puis on utilise la mĂ©thode evaluateAll, qui permet d’exĂ©cuter du code dans le navigateur — trĂšs pratique pour le scraping. Ici, on l’utilise pour extraire facilement les attributs href et rĂ©cupĂ©rer les URLs (ligne 10).
  • On affiche les URLs dans le terminal (ligne 12).
  • Enfin, on ferme tout ce qui est liĂ© Ă  chromium (lignes 14–16).

Dans votre terminal, vous devriez voir toutes les URLs affichées !

sda/main.ts
import { chromium } from "playwright-chromium";
 
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.ourcommons.ca/Members/en/search?parliament");
 
const urls = await page.locator(".ce-mip-mp-tile")
  .evaluateAll((elements) => elements.map((a) => a.href));
 
console.log(urls);
 
await page.close();
await context.close();
await browser.close();

Toutes les URLs des pages personnelles des députés.

Maintenant que nous avons toutes les URLs des pages personnelles des dĂ©putĂ©s, nous pouvons itĂ©rer sur chacune d’elles et extraire des informations pour chaque dĂ©putĂ©.

Si vous avez accĂšs Ă  une IA dans les outils de dĂ©veloppement de votre navigateur (comme dans Chrome) et que vous ne savez pas trop quel code Ă©crire, vous pouvez lui poser la question directement. C’est plutĂŽt pratique. Bien sĂ»r, faites attention Ă  ce que vous demandez : ces donnĂ©es sont envoyĂ©es sur Internet (ici, Ă  Google) et les rĂ©ponses ne sont pas toujours justes !

Par exemple, dans la capture ci-dessous, j’ai inspectĂ© le nom du dĂ©putĂ©, puis j’ai fait un clic droit dessus dans le code HTML et j’ai cliquĂ© sur Ask AI. Dans la discussion en bas, j’ai demandĂ© : How can I retrieve the text content with Playwright?. Et le code fourni fonctionne !

IA utilisée pour récupérer le contenu textuel dans Playwright.

Dans le code ci-dessous, on extrait le nom, le parti, la circonscription, la province ou le territoire, et bien sĂ»r la langue d’usage du dĂ©putĂ©.

Ici, on parcourt toutes les pages des dĂ©putĂ©s ! Donc on s’assure d’ajouter un petit dĂ©lai (500 ms) Ă  la fin de chaque itĂ©ration pour Ă©viter de surcharger le serveur du site web avec trop de requĂȘtes. De nombreux sites vous bloqueront si vous les visitez trop frĂ©quemment.

Dans votre terminal, vous devriez voir les donnĂ©es s’afficher au fur et Ă  mesure de l’extraction !

sda/main.ts
import { chromium } from "playwright-chromium";
 
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.ourcommons.ca/Members/en/search?parliament");
 
const urls = await page.locator(".ce-mip-mp-tile")
  .evaluateAll((elements) => elements.map((a) => a.href));
 
for (const url of urls) {
  console.log(`\n${url}`);
  await page.goto(url);
  const name = await page.locator("h1").textContent();
  const party = await page.locator(".mip-mp-profile-caucus").textContent();
  const district = await page.locator("dd > a").textContent();
  const province = await page.locator("dl > dd:nth-child(6)").textContent();
  const language = await page.locator("dl > dd:nth-of-type(4)").textContent();
 
  const data = { name, party, district, province, language };
  console.log(data);
 
  await page.waitForTimeout(500);
}
 
await page.close();
await context.close();
await browser.close();

Données extraites pour les députés.

Quand j’extrais des donnĂ©es sur des pages comme celle-ci, j’aime gĂ©nĂ©ralement ajouter quelques Ă©lĂ©ments supplĂ©mentaires au script :

  • Un compteur pour estimer le temps restant.
  • Un log au dĂ©but de chaque boucle pour savoir quel Ă©lĂ©ment est en cours de traitement.
  • Une Ă©tape de mise en cache pour Ă©viter de re-tĂ©lĂ©charger les donnĂ©es dĂ©jĂ  extraites.
sda/main.ts
import { chromium } from "playwright-chromium";
import { exists } from "@std/fs";
import { DurationTracker } from "@nshiab/journalism";
 
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.ourcommons.ca/Members/en/search?parliament");
 
const urls = await page.locator(".ce-mip-mp-tile")
  .evaluateAll((elements) => elements.map((a) => a.href));
 
const tracker = new DurationTracker(urls.length, { prefix: "Remaining: " });
 
for (let i = 0; i < urls.length; i++) {
  const url = urls[i];
  const id = url.split("/").pop();
  const filePath = `./sda/data/${id}.json`;
 
  console.log(`\nProcessing ${i + 1} of ${urls.length}: ${url}`);
 
  if (await exists(filePath)) {
    console.log(`File already exists: ${filePath}`);
  } else {
    tracker.start();
 
    await page.goto(url);
    const name = await page.locator("h1").textContent();
    const party = await page.locator(".mip-mp-profile-caucus").textContent();
    const district = await page.locator("dd > a").textContent();
    const province = await page.locator("dl > dd:nth-child(6)").textContent();
    const language = await page.locator("dl > dd:nth-of-type(4)").textContent();
 
    const data = { name, party, district, province, language };
 
    const json = JSON.stringify(data, null, 2);
    await Deno.writeTextFile(filePath, json);
 
    await page.waitForTimeout(500);
    tracker.log();
  }
}
 
await page.close();
await context.close();
await browser.close();

Maintenant, lorsque vous exĂ©cutez le script, vous avez une meilleure idĂ©e de sa vitesse d’exĂ©cution et du temps estimĂ© qu’il lui reste. Et surtout, s’il plante Ă  un moment pour n’importe quelle raison, vous n’avez pas besoin de tout rĂ©extraire. Les donnĂ©es prĂ©cĂ©demment extraites ont Ă©tĂ© sauvegardĂ©es sous forme de fichiers JSON !

DonnĂ©es extraites pour les dĂ©putĂ©s, avec plus d’informations sur l’extraction.

Par dĂ©faut, Playwright fonctionne en mode headless, ce qui signifie que le navigateur automatisĂ© ne s’affiche pas. Mais si vous voulez voir votre script en action, vous pouvez passer l’option { headless: false }.

sda/main.ts
import { chromium } from "playwright-chromium";
import { exists } from "@std/fs";
import { DurationTracker } from "@nshiab/journalism";
 
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.ourcommons.ca/Members/en/search?parliament");
 
const urls = await page.locator(".ce-mip-mp-tile")
  .evaluateAll((elements) => elements.map((a) => a.href));
 
const tracker = new DurationTracker(urls.length, { prefix: "Remaining: " });
 
for (let i = 0; i < urls.length; i++) {
  const url = urls[i];
  const id = url.split("/").pop();
  const filePath = `./sda/data/${id}.json`;
 
  console.log(`\nProcessing ${i + 1} of ${urls.length}: ${url}`);
 
  if (await exists(filePath)) {
    console.log(`File already exists: ${filePath}`);
  } else {
    tracker.start();
 
    await page.goto(url);
    const name = await page.locator("h1").textContent();
    const party = await page.locator(".mip-mp-profile-caucus").textContent();
    const district = await page.locator("dd > a").textContent();
    const province = await page.locator("dl > dd:nth-child(6)").textContent();
    const language = await page.locator("dl > dd:nth-of-type(4)").textContent();
 
    const data = { name, party, district, province, language };
 
    const json = JSON.stringify(data, null, 2);
    await Deno.writeTextFile(filePath, json);
 
    await page.waitForTimeout(500);
    tracker.log();
  }
}
 
await page.close();
await context.close();
await browser.close();

Et maintenant, vous voyez votre code en action ! Cela peut ĂȘtre trĂšs utile pour dĂ©boguer un script qui ne fonctionne pas comme prĂ©vu.

Playwright avec le mode headless désactivé.

Enfin, vous pouvez charger tous ces fichiers JSON avec SDA et traiter vos donnĂ©es. Par exemple, ici, une fois l’extraction terminĂ©e, on compte le nombre de dĂ©putĂ©s selon leur langue d’usage.

sda/main.ts
import { chromium } from "playwright-chromium";
import { exists } from "@std/fs";
import { DurationTracker } from "@nshiab/journalism";
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.ourcommons.ca/Members/en/search?parliament");
 
const urls = await page.locator(".ce-mip-mp-tile")
  .evaluateAll((elements) => elements.map((a) => a.href));
 
const tracker = new DurationTracker(urls.length, { prefix: "Remaining: " });
 
for (let i = 0; i < urls.length; i++) {
  const url = urls[i];
  const id = url.split("/").pop();
  const filePath = `./sda/data/${id}.json`;
 
  console.log(`\nProcessing ${i + 1} of ${urls.length}: ${url}`);
 
  if (await exists(filePath)) {
    console.log(`File already exists: ${filePath}`);
  } else {
    tracker.start();
 
    await page.goto(url);
    const name = await page.locator("h1").textContent();
    const party = await page.locator(".mip-mp-profile-caucus").textContent();
    const district = await page.locator("dd > a").textContent();
    const province = await page.locator("dl > dd:nth-child(6)").textContent();
    const language = await page.locator("dl > dd:nth-of-type(4)").textContent();
 
    const data = { name, party, district, province, language };
 
    const json = JSON.stringify(data, null, 2);
    await Deno.writeTextFile(filePath, json);
 
    await page.waitForTimeout(500);
    tracker.log();
  }
}
 
await page.close();
await context.close();
await browser.close();
 
const sdb = new SimpleDB();
 
const MPs = sdb.newTable("MPs");
await MPs.loadData("sda/data/*.json");
await MPs.summarize({
  categories: ["party", "language"],
});
await MPs.wider("party", "count");
await MPs.logTable();
 
await sdb.done();

Chargement et résumé des données extraites sur les députés.

Félicitations ! Vous savez maintenant comment utiliser Playwright pour extraire des données de pages web !

Pages complexes

Certains sites web sont plus compliquĂ©s Ă  extraire. Il n’est pas toujours possible de rĂ©cupĂ©rer simplement une liste d’URLs. Parfois, il faut cliquer sur plusieurs menus et boutons pour accĂ©der aux donnĂ©es souhaitĂ©es.

Dans l’exemple ci-dessous, nous allons explorer le site d’Élections Canada. Vous pourriez tĂ©lĂ©charger toutes les donnĂ©es assez facilement, mais cette interface publique est une excellente occasion d’apprendre Ă  extraire des donnĂ©es depuis des pages web plus complexes.

Disons que nous voulons rĂ©cupĂ©rer les dĂ©penses dĂ©clarĂ©es des candidats aux Ă©lections fĂ©dĂ©rales canadiennes. Il faut cliquer sur plusieurs options sur la page d’accueil.

SĂ©lection de plusieurs options sur le site d’Élections Canada.

Cela nous amĂšne ensuite Ă  une nouvelle page, avec encore de nombreuses options Ă  choisir.

SĂ©lection d’autres options sur le site d’Élections Canada.

Et enfin, nous avons accÚs aux données, mais il faut encore parcourir un menu pour extraire les données de chaque candidat.

DonnĂ©es des candidats sur le site d’Élections Canada.

Heureusement, c’est assez facile Ă  faire avec les mĂ©thodes selectOption et click de Playwright !

Le code ci-dessous utilise l’option { headless: false }, plusieurs appels à await page.waitForTimeout(500), et un scrollIntoViewIfNeeded pour que vous puissiez voir le script interagir avec la page.

sda/main.ts
import { chromium } from "playwright-chromium";
 
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.elections.ca/WPAPPS/WPF/EN/Home/Index");
await page.selectOption("#actsList", "CC_C76");
await page.waitForTimeout(500);
await page.selectOption("#CCEventList", "53");
await page.waitForTimeout(500);
await page.selectOption("#reportTypeList", "8");
await page.waitForTimeout(500);
await page.click("#SearchButton");
 
await page.click("#ReturnStatusList2");
await page.waitForTimeout(500);
await page.click("#ReportOptionList1");
await page.waitForTimeout(500);
await page.click("#button3");
await page.waitForTimeout(500);
await page.click("#SelectAllCandidates");
await page.waitForTimeout(500);
await page.click("#SearchSelected");
 
const selectElement = page.locator("#SelectedClientId");
const optionCount = await selectElement.locator("option").count();
 
for (let i = 0; i < optionCount; i++) {
  const optionValue = await selectElement.locator("option").nth(i)
    .getAttribute("value");
 
  await selectElement.selectOption(optionValue);
  await page.click("#ReportOptions");
 
  await page.locator("#sumrpt").scrollIntoViewIfNeeded();
 
  await page.waitForTimeout(500);
}
 
await page.close();
await context.close();
await browser.close();

ItĂ©ration sur les candidats sur le site d’Élections Canada.

Maintenant que nous sommes capables d’itĂ©rer sur les candidats, nous pouvons extraire les donnĂ©es du tableau, comme le nom du candidat, la circonscription, le parti et les dĂ©penses. Nous pouvons aussi mesurer la durĂ©e de l’extraction et mettre les donnĂ©es en cache.

Pour Ă©viter les problĂšmes, assurez-vous de vider votre dossier sda/data avant d’exĂ©cuter ce nouveau script.

sda/main.ts
import { chromium } from "playwright-chromium";
import { exists } from "@std/fs";
import { DurationTracker } from "@nshiab/journalism";
 
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.elections.ca/WPAPPS/WPF/EN/Home/Index");
await page.selectOption("#actsList", "CC_C76");
await page.selectOption("#CCEventList", "53");
await page.selectOption("#reportTypeList", "8");
await page.click("#SearchButton");
 
await page.click("#ReturnStatusList2");
await page.click("#ReportOptionList1");
await page.click("#button3");
await page.click("#SelectAllCandidates");
await page.click("#SearchSelected");
 
const selectElement = page.locator("#SelectedClientId");
const optionCount = await selectElement.locator("option").count();
 
const tracker = new DurationTracker(optionCount, { prefix: "Remaining: " });
 
for (let i = 0; i < optionCount; i++) {
  tracker.start();
  const optionValue = await selectElement.locator("option").nth(i)
    .getAttribute("value");
 
  const path = `sda/data/${optionValue}.json`;
 
  if (await exists(path)) {
    console.log(`File already exists: ${path}`);
  } else {
    console.log(`\nRetrieving ${optionValue} (${i + 1}/${optionCount})`);
    await selectElement.selectOption(optionValue);
    await page.click("#ReportOptions");
 
    const name = await page.textContent("#ename1");
    if (name === null) {
      throw new Error("name is null");
    }
    const partyAndDIstrict = await page.textContent("#partydistrict1");
    if (partyAndDIstrict === null) {
      throw new Error("partyAndDIstrict is null");
    }
    const party = partyAndDIstrict.split("/")[0].trim();
    const district = partyAndDIstrict.split("/")[1].trim();
    const expenses = await page.textContent(
      "#sumrpt > tbody > tr:nth-child(16) > td > span",
    );
 
    console.log({
      name,
      party,
      district,
      expenses,
    });
 
    await Deno.writeTextFile(
      path,
      JSON.stringify([{
        name,
        party,
        district,
        expenses,
      }]),
    );
 
    await page.waitForTimeout(500);
    tracker.log();
  }
}
 
await page.close();
await context.close();
await browser.close();

Mise en cache des donnĂ©es des candidats sur le site d’Élections Canada.

Et enfin, comme d’habitude, on peut utiliser SDA pour charger toutes les donnĂ©es extraites et les analyser. Par exemple, on pourrait calculer les dĂ©penses moyennes par parti.

sda/main.ts
import { chromium } from "playwright-chromium";
import { exists } from "@std/fs";
import { DurationTracker } from "@nshiab/journalism";
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
 
await page.goto("https://www.elections.ca/WPAPPS/WPF/EN/Home/Index");
await page.selectOption("#actsList", "CC_C76");
await page.selectOption("#CCEventList", "53");
await page.selectOption("#reportTypeList", "8");
await page.click("#SearchButton");
 
await page.click("#ReturnStatusList2");
await page.click("#ReportOptionList1");
await page.click("#button3");
await page.click("#SelectAllCandidates");
await page.click("#SearchSelected");
 
const selectElement = page.locator("#SelectedClientId");
const optionCount = await selectElement.locator("option").count();
 
const tracker = new DurationTracker(optionCount, { prefix: "Remaining: " });
 
for (let i = 0; i < optionCount; i++) {
  tracker.start();
  const optionValue = await selectElement.locator("option").nth(i)
    .getAttribute("value");
 
  const path = `sda/data/${optionValue}.json`;
 
  if (await exists(path)) {
    console.log(`File already exists: ${path}`);
  } else {
    console.log(`\nRetrieving ${optionValue} (${i + 1}/${optionCount})`);
    await selectElement.selectOption(optionValue);
    await page.click("#ReportOptions");
 
    const name = await page.textContent("#ename1");
    if (name === null) {
      throw new Error("name is null");
    }
    const partyAndDIstrict = await page.textContent("#partydistrict1");
    if (partyAndDIstrict === null) {
      throw new Error("partyAndDIstrict is null");
    }
    const party = partyAndDIstrict.split("/")[0].trim();
    const district = partyAndDIstrict.split("/")[1].trim();
    const expenses = await page.textContent(
      "#sumrpt > tbody > tr:nth-child(16) > td > span",
    );
 
    console.log({
      name,
      party,
      district,
      expenses,
    });
 
    await Deno.writeTextFile(
      path,
      JSON.stringify([{
        name,
        party,
        district,
        expenses,
      }]),
    );
 
    await page.waitForTimeout(100);
    tracker.log();
  }
}
 
await page.close();
await context.close();
await browser.close();
 
const sdb = new SimpleDB();
const returns = sdb.newTable("returns");
await returns.loadData("sda/data/*.json");
await returns.convert({ expenses: "number" }, { try: true });
await returns.summarize({
  values: "expenses",
  categories: "party",
  summaries: ["mean", "count"],
  decimals: 0,
});
await returns.sort({ mean: "desc" });
await returns.logTable(13);

Résumé des dépenses des candidats avec SDA.

Extraction via des APIs non documentées

Parfois, au lieu d’extraire Ă  partir du code HTML, vous pouvez utiliser directement l’API qui alimente la page. Par exemple, le site de Yahoo Finance affiche beaucoup de donnĂ©es qu’on pourrait extraire avec Playwright. Mais une autre technique consiste Ă  repĂ©rer l’API que la page appelle pour obtenir ses donnĂ©es.

💡

API signifie Application Programming Interface. Sur le web, les API sont souvent utilisĂ©es pour transfĂ©rer des donnĂ©es. Lorsque vous appelez un point d’accĂšs API (via une URL et parfois des paramĂštres), l’API renvoie les donnĂ©es correspondantes. Les API sont trĂšs utiles pour les sites affichant des donnĂ©es en temps rĂ©el, entre autres. Au lieu de reconstruire et republier le site avec de nouvelles donnĂ©es — ce qui peut ĂȘtre lent et coĂ»teux — il suffit de mettre Ă  jour les points d’accĂšs de l’API. Les rĂ©ponses des API sont souvent en JSON, mais elles peuvent aussi ĂȘtre en CSV, XML et d’autres formats.

Sur la page d’accueil, vous pouvez chercher une entreprise cotĂ©e en bourse. Par exemple, recherchez Apple et cliquez sur le rĂ©sultat correspondant.

Une capture d’écran montrant le site Yahoo Finance.

Vous arriverez sur la page boursiùre d’Apple. Sur la gauche, cliquez sur Historical Data.

Une capture d’écran montrant le site Yahoo Finance.

Ces donnĂ©es ne sortent pas de nulle part. Elles viennent d’une API qui alimente la page. Jetons un coup d’Ɠil sous le capot pour en trouver la source. 🧐

Une capture d’écran montrant le site Yahoo Finance.

Note : j’utiliserai Google Chrome pour les Ă©tapes suivantes, mais vous pouvez faire la mĂȘme chose avec Firefox ou Safari.

Ouvrez les Outils de dĂ©veloppement et cliquez sur l’onglet Network.

Une capture d’écran montrant le site Yahoo Finance avec les outils de dĂ©veloppement ouverts.

Cet onglet affiche toutes les requĂȘtes faites par la page. Lorsqu’elle se charge, elle a besoin de diverses ressources comme des polices, images, styles
 et des donnĂ©es ! Toutes ces requĂȘtes sont listĂ©es ici, et vous pouvez les explorer.

Dans notre cas, on s’intĂ©resse aux donnĂ©es boursiĂšres d’Apple affichĂ©es dans un tableau sur la page.

RafraĂźchissez la page, puis sĂ©lectionnez Ă  nouveau l’option Max pour rĂ©cupĂ©rer toutes les donnĂ©es disponibles. Cherchez une requĂȘte contenant AAPL, le symbole boursier d’Apple. C’est aussi le symbole utilisĂ© dans l’URL de la page, donc c’est un bon indice.

Vous remarquerez une ou plusieurs requĂȘtes fetch qui commencent par AAPL. Ça semble trĂšs prometteur !

Une capture d’écran montrant le site Yahoo Finance avec les requĂȘtes rĂ©seau dĂ©taillĂ©es.

Faites un clic droit sur l’une d’elles et ouvrez-la dans un nouvel onglet. Wow ! Vous reconnaissez cette syntaxe ? C’est du JSON ! Et il y a beaucoup de donnĂ©es. 😏

Voici le lien au cas oĂč vous en auriez besoin.

Une capture d’écran montrant l’API de Yahoo Finance.

Si vous regardez de prùs l’URL, vous remarquerez des paramùtres comme symbol, interval, period1 et period2. Il y a aussi des paramùtres region et lang, qui peuvent varier selon votre emplacement.

https://query1.finance.yahoo.com/v8/finance/chart/AAPL?events=capitalGain%7Cdiv%7Csplit&formatted=true&includeAdjustedClose=true&interval=1d&period1=345479400&period2=1738778777&symbol=AAPL&userYfid=true&lang=en-CA&region=CA

Cela signifie que vous pouvez appeler cette URL pour extraire les données de Yahoo Finance, simplement en changeant les paramÚtres. Et beaucoup de sites web fonctionnent de cette maniÚre, via une API.

Notez que cet exemple vient du projet Simulateur boursier 📈. Allez le voir si vous voulez en savoir plus sur l’utilisation d’APIs non documentĂ©es dans vos projets.

Conclusion

Quel parcours ! Nous avons couvert beaucoup de choses dans cette leçon et j’espĂšre que vous l’avez trouvĂ©e utile. Le web scraping est une compĂ©tence essentielle pour rĂ©colter des donnĂ©es, surtout pour les journalistes computationnels.

Mais souvenez-vous : assurez-vous de toujours respecter les lois en vigueur, et ne mettez pas trop de pression sur les serveurs hébergeant les données qui vous intéressent.

Bon scraping ! đŸ€ 

Vous avez aimé ? Vous voulez ĂȘtre prĂ©venu quand de nouvelles leçons sont publiĂ©es ? Abonnez-vous Ă  l'infolettre ✉ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Contactez-moi si vous avez des questions.