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.
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
.
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.
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.
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();
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.
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();
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 !
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();
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.
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();
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.
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 ?
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
.
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 nouveaubrowser
, puis un nouveaucontext
, et enfin unenewPage
(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 attributshref
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 !
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();
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 !
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 !
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();
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.
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 !
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 }
.
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.
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.
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();
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.
Cela nous amĂšne ensuite Ă une nouvelle page, avec encore de nombreuses options Ă choisir.
Et enfin, nous avons accÚs aux données, mais il faut encore parcourir un menu pour extraire les données de chaque candidat.
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.
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();
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.
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();
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.
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);
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.
Vous arriverez sur la page boursiĂšre dâApple. Sur la gauche, cliquez sur Historical Data.
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. đ§
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.
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 !
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.
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®ion=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 ! đ€