-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
358 lines (313 loc) · 12 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
import fs from "fs";
import pLimit from "p-limit";
import * as cheerio from "cheerio";
import ora, { oraPromise } from "ora";
import chalk from "chalk";
import readline from "readline";
const MAX_CONCURRENT_REQUESTS = 15; // NOTA: No és recomanable fer més de 15 peticions simultànies a la web de la FEEC. Això pot saturar la web i provocar més lentitud
const FEEC_API = "https://www.feec.cat/wp-admin/admin-ajax.php"; // URL de l'API de la FEEC
const ESSENCIAL_TEXT = "Cim essencial"; // Aquest text apareix a la web de la FEEC quan un cim és essencial
const OUTPUT_FILE = "muntanyesRepte100CimsFEEC.json"; // Nom del fitxer on es guardaran les dades
// Crear una interfície per a llegir la consola
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Escurçar el console.log per a un ús més fàcil
const log = console.log;
// Limitar el nombre de peticions simultànies a la web per obtenir informació extra de les muntanyes
const limit = pLimit(MAX_CONCURRENT_REQUESTS);
/**
* Aquesta funció realitza una petició POST a l'API de la FEEC per obtenir la informació dels 100 cims
*
* @param {Number} pageNumber - Número de la pàgina de l'API que volem obtenir
* @param {String} nonce - Aquesta cadena de text és un token de seguretat que s'utilitza per prevenir atacs CSRF
* @returns {Promise<String>} - Retorna la resposta de l'API en format de text
*/
const getApiData = async (pageNumber, nonce) => {
const response = await fetch(FEEC_API, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `action=load_100cims&nonce=${nonce}&cims_query=cims_actius¤t_page=${pageNumber}`,
});
return await response.text();
};
/**
* Aquesta funció realitza una petició GET a la web de la FEEC per obtenir més informació d'una muntanya (Geolocalització, etc.)
*
* @param {String} url - URL de la muntanya de la FEEC
* @returns {Promise<String>} - Retorna la resposta de la web en format de text
*/
const getMountainDataFromWebsite = async (url) => {
const response = await fetch(url);
return await response.text();
};
/**
* Aquesta funció rep les dades de l'API de la FEEC i retorna un array amb la informació bàsica de les muntanyes
*
* @param {String} data - Dades de l'API de la FEEC
* @returns {Array<Object>} - Retorna un array amb la informació de les muntanyes
*/
const parseMountainsFromAPI = (data) => {
const $ = cheerio.load(data);
const items = $(".item-100cims");
if (!items.length) return [];
return items
.map((_, element) => {
const url = $(element).attr("href");
const image = $(element).find("img").attr("src");
const name = $(element).find("h3").text();
const height = $(element).find("h5").first().text();
const region = $(element).find("h5").last().text();
const essencial = $(element).find("strong").text() === ESSENCIAL_TEXT;
return {
id: url
.split("/")
.filter((segment) => segment)
.pop(),
url,
image,
name,
height: +height.match(/\d+/)[0],
region,
essencial,
};
})
.get();
};
/**
* Aquesta funció obté el nonce de la web de la FEEC. Aquest nonce és necessari per fer peticions a l'API de la FEEC
* @returns {Promise<String>} - Retorna el nonce de la web de la FEEC
*/
const getAPINonce = async () => {
const response = await fetch("https://www.feec.cat/activitats/100-cims/");
const data = await response.text();
const $ = cheerio.load(data);
const scriptContent = $("script")
.filter((_, el) => $(el).html().includes("var ajaxcustom"))
.html();
const nonce = scriptContent.match(/"nonce":"([a-zA-Z0-9]+)"/)[1];
return nonce;
};
/**
* Aquesta funció rep les dades de la web de la FEEC i retorna la latitud i longitud de la muntanya
* @param {String} data - Dades de la web de la FEEC
* @returns {Object} - Retorna un objecte amb la latitud i longitud de la muntanya
*/
const parseMountainFromWebsite = (data) => {
const $ = cheerio.load(data);
const container = $(".row.no-gutters.fw-light.lh-1-2");
const latitude = container
.find('div:contains("Latitud:")')
.next()
.text()
.trim()
.replace("º", "");
const longitude = container
.find('div:contains("Longitud:")')
.next()
.text()
.trim()
.replace("º", "");
return { latitude, longitude };
};
/**
* Aquesta funció obté el nombre total de pàgines de l'API de la FEEC.
* Aquest nombre de pàgines ens servirà per saber quantes pàgines hem de recórrer per obtenir totes les muntanyes
*
* @param {String} nonce - Aquesta cadena de text és un token de seguretat que s'utilitza per prevenir atacs CSRF
* @returns {Promise<String>} - Retorna el nombre total de pàgines de l'API de la FEEC
*/
const getTotalPages = async (nonce) => {
const data = await getApiData(1, nonce);
const $ = cheerio.load(data);
return $("a").last().attr("data-page");
};
/**
* Aquesta funció rep el nonce de la web de la FEEC i el nombre total de pàgines de l'API de la FEEC.
* Recorre totes les pàgines de l'API de la FEEC per obtenir la informació bàsica de totes les muntanyes
*
* @param {String} nonce - Aquesta cadena de text és un token de seguretat que s'utilitza per prevenir atacs CSRF
* @param {Number} totalPages - Nombre total de pàgines de l'API de la FEEC
* @returns {Promise<Array<Object>>} - Retorna un array amb la informació bàsica de les muntanyes
*/
const getBasicInfoMountain = async (nonce, totalPages) => {
let pageNumber = 1;
const requestToAPI = [];
while (pageNumber <= totalPages) {
// Push the request to the API to the stack of promises
requestToAPI.push(getApiData(pageNumber, nonce));
pageNumber++;
}
const responses = await Promise.all(requestToAPI);
const basicMountain = responses
.map((data) => parseMountainsFromAPI(data))
.flat();
return basicMountain;
};
/**
* Aquesta funció rep les dades de les muntanyes i realitza una petició a la web de la FEEC per obtenir més informació de les muntanyes
*
* @param {Array<Object>} mountains - Dades de les muntanyes
* @returns {Promise<Array<Object>>} - Retorna un array amb la informació de les muntanyes amb la informació extra
*/
const getExtraInfoMountain = async (mountains) => {
const requestToWebsite = [];
for (const mountain of mountains) {
// Get the data from the website of the mountain
requestToWebsite.push(
limit(() => getMountainDataFromWebsite(mountain.url))
);
}
const responses = await Promise.all(requestToWebsite);
const mountainsDataTest = responses.map((data, index) => {
const parsedMountainDetail = parseMountainFromWebsite(data);
return { ...mountains[index], ...parsedMountainDetail };
});
return mountainsDataTest;
};
/**
* Aquesta funció rep les dades de les muntanyes i les guarda en un fitxer JSON
* @param {Array<Object>} data - Dades de les muntanyes
* @returns {void}
* @throws {Error} - Llança un error si no es pot guardar les dades en el fitxer JSON
*/
const saveDataToJSONFile = (data) => {
fs.writeFile(OUTPUT_FILE, JSON.stringify(data), function (err) {
if (err) throw err;
});
};
/**
* Aquesta funció envia un missatge per consola a l'usuari i espera una resposta
*
* @param {String} question - Se li fa una pregunta a l'usuari
* @returns {Promise<String>} - Retorna la resposta de l'usuari
*/
const askQuestionToUser = (question) => {
return new Promise((resolve) => rl.question(question, resolve));
};
/**
* Aquesta funció comprova si l'usuari vol continuar amb l'execució de l'scraper
* Si l'usuari no vol continuar, l'scraper s'atura
* @returns {void}
*/
const checkIfUserWantsToContinue = async () => {
const answer = await askQuestionToUser(
"Vols continuar amb l'execució del scraper, sota la teva responsabilitat? (S/N) "
);
if (answer.toUpperCase() !== "S") {
log(
chalk.redBright(
`Has decidit aturar l'execució. Si la vols tornar a executar, torna a executar!`
)
);
process.exit(0);
}
};
/**
* Funció principal que executa el scraper, aquesta funció fa servir les funcions anteriors per obtenir les dades de les muntanyes.
* un cop obtingudes les dades les guarda en un fitxer JSON
*
* @returns {void}
*/
const getMountains = async () => {
try {
log(
chalk.greenBright(`
/\\ /\\
/ \\ Benvingut, ets a punt d'obtenir les dades de / \\
/ \\ les muntanyes del repte dels 100 cims de la FEEC! / \\
/______\\___________________________________________________/______\\
`)
);
log(
chalk.redBright(
`Si us plau, fes servir aquesta eina amb responsabilitat! Ja que pot saturar la web de la FEEC! \n`
)
);
log(
chalk.yellowBright(
`El creador d'aquesta eina no es fa responsable de l'ús que se'n pugui fer! \n`
)
);
// Preguntem a l'usuari si vol continuar amb l'execució de l'scraper
await checkIfUserWantsToContinue();
log(
chalk.greenBright(
`\nComencem a obtenir les dades de les muntanyes del repte dels 100 cims de la FEEC! \n`
)
);
// Obtenir el nonce de l'API de la FEEC, aquest nonce és necessari per fer peticions
const nonce = await oraPromise(getAPINonce(), {
text: "Obtenint el nonce per a poder fer peticions a l'API de la FEEC...",
successText: "Obtenir el nonce OK!",
failText: "Alguna cosa ha anat malament en obtenir el nonce \n",
});
// Obtenir el nombre total de pàgines de l'API de la FEEC, això ens servirà per saber quantes pàgines hem de recórrer per obtenir totes les muntanyes
const totalPages = await oraPromise(getTotalPages(nonce), {
text: "Obtenint el nombre total de pàgines de l'API de la FEEC...",
successText: "Obtenir el nombre total de pàgines de l'API OK!",
failText:
"Alguna cosa ha anat malament en obtenir el nombre total de pàgines \n",
});
// Obtenir la informació bàsica de totes les muntanyes
const basicMountainsInfo = await oraPromise(
getBasicInfoMountain(nonce, totalPages),
{
text: "Obtenint la informació bàsica de totes les muntanyes...",
successText: "Informació bàsica de les muntanyes OK!",
failText:
"Alguna cosa ha anat malament en obtenir la informació bàsica de les muntanyes \n",
}
);
log(
chalk.green(
`\nS'han obtingut les dades de ${basicMountainsInfo.length} muntanyes correctament! \n`
)
);
log(
chalk.yellowBright(
`Pot ser que el següent pas trigui una bona estona, ja que s'han de fer moltes peticions. Relaxa't i pren-te un cafè! \n`
)
);
// Obtenir la informació extra de totes les muntanyes
const mountainsData = await oraPromise(
getExtraInfoMountain(basicMountainsInfo),
{
text: "Obtenint la informació extra de totes les muntanyes...",
successText: "Informació extra de les muntanyes OK!",
failText:
"Alguna cosa ha anat malament en obtenir la informació extra de les muntanyes \n",
}
);
saveDataToJSONFile(mountainsData);
// Guardar les dades en un fitxer JSON
const spinner = ora(`Desant totes les dades en un fitxer JSON...`).start();
saveDataToJSONFile(mountainsData);
spinner.succeed(
`Totes les dades s'han desat correctament. Pots consultar-les al fitxer ${OUTPUT_FILE}! \n`
);
log(
chalk.greenBright(
`Espero que aquesta eina t'hagi estat d'utilitat! Si tens algun dubte o suggeriment pots contactar amb mi a GitHub: @mcmontseny \n`
)
);
log(chalk.greenBright(`Molta sort amb el repte dels 100 cims! \n`));
} catch (error) {
log(
chalk.redBright(
`Alguna cosa ha anat malament a l'execució de l'scraper! \n`
)
);
console.error("Error:", error);
log(
chalk.redBright(
`Si us plau, contacta amb el creador de l'eina a GitHub: @mcmontseny \n`
)
);
}
};
/** Execució de la funció principal */
getMountains();