Написал парсер сайта на Node.js
Tags: node, html
Published: 2022-01-08 09:24:59

По прошлым публикациям видно, что у меня есть некий телеграм-бот. Этот бот имеет такую фичу: поздравляет с каким-нибудь праздником на этот день. Всякий раз, когда нужно было вызвать эту фичу, он идёт на сайт со списком праздников, берёт заданную дату и выводит название праздника. Настало время, когда такая зависимость от сайта меня перестала устраивать. Решено забрать от сайта все праздники, записать их куда-нибудь по-ближе.
Итак алгоритм этой работы будет таким:
1. В цикле от 1 января по 31 декабря _(включая 29 февраля)_
2. Сформировать список ссылок для первых **COUNT_REQUEST** дней 
3. Запросить праздники для первых **COUNT_REQUEST** дней
4. Запомнить праздники
5. Подождать **DELAY_REQUEST** секунд _(ведь у меня цель не задосить сайт, а парсить)_
6. Счётчик цикла увеличить на **COUNT_REQUEST**
7. После выхода из цикла записать считанные праздники в JSON.

Перейдём к реализации на JS

### Первый и шестой пункт цикла:
```js
const COUNT_REQUEST = 2;
const DELAY_REQUEST = 20000;

/**
 * Прибавить в дате countDays дней
 * @param {Date} date
 * @param {number} countDays
 * @returns {Date}
 */
export function addDay(date, countDays = 1) {
  const newDate = new Date(date);
  return new Date(newDate.setDate(newDate.getDate() + countDays));
}


const startDate = new Date("2020-01-01");
const endDate = new Date("2020-12-31");

let loop = new Date(startDate);
while (loop <= endDate) {
  loop = addDay(loop, COUNT_REQUEST);
  await delay(DELAY_REQUEST);
}
```
### 2. Сформировать список ссылок для первых **COUNT_REQUEST** дней

```js
/**
 * @typedef {Object} UrlData
 * @property {Date} date - Запрашиваемый день
 * @property {string} url - Ссылка
 */

/**
 * Получить массив ссылок для countDays дней
 * @param {Date} startDate С какой даты начинать делать ссылки
 * @param {number} countDays Сколько ссылок спрашивать
 * @param {Date} endDate За какую дату не заходить
 * @returns {UrlData}
 */
export function getUrls(startDate, countDays, endDate) {
  //--- Текст функции
  return urls;
}
```

### 3. Запросить праздники для первых **COUNT_REQUEST** дней
Для этого понадобятся два пакета [node-fetch](https://www.npmjs.com/package/node-fetch) и [node-html-parser](https://www.npmjs.com/package/node-html-parser).
Для реализации использовал фичу из 16 версии nodejs [AbortController](https://www.nearform.com/blog/using-abortsignal-in-node-js/) . Хотя и не полностью как в статье сделал,- setTimeout у меня по старинке запускается.
```js
import fetch from "node-fetch";
import { parse } from "node-html-parser";

/**
 * Запрос списка праздников
 * @param {string} url
 * @param {Date} date
 * @returns {String[]}
 */
export async function getHolydays(url, date) {
  // Для отмены фетча
  const cancelFetch = new AbortController();
  // Промис запроса к сайту
  const promise = fetch(url, {
    timeout: REQUEST_TIMEOUT,
    signal: cancelFetch.signal,
  });
  // Время ожидания
  const timeout = setTimeout(() => {
    cancelFetch.abort();
  }, WAIT_REQUEST_TIMEOUT);
  try {
    const response = await promise;
    // Получить текст HTML
    const htmlContent = await response.text();
    // Получить структуру DOM
    const root = parse(htmlContent);
    // Массив праздников: DOM-элементы
    const source = root.querySelectorAll(".holydays >span");
    // Массив праздников: текст
    const holidays = source.map((element) => element.textContent);
    return { holidays, day: date.getDate(), month: 1 + date.getMonth() };
  } catch (e) {
    console.log("FetchError :>> ", date);
    return null;
  } finally {
    clearTimeout(timeout);
  }
}

// Получить список праздников из массива ссылок
    const promisesOfHolidays = await Promise.all(
      urlsData.map(async (ud) => await getHolydays(ud.url, ud.date))
    );
```

### 4. Запомнить праздники
Результат собирается в массив
```js
let holidayData = [];
///
const promisesOfHolidays = await Promise.all(
    urlsData.map(async (ud) => await getHolydays(ud.url, ud.date))
    );
holidayData = [
      ...holidayData,
      ...promisesOfHolidays.filter((r) => r !== null),
    ];
```

### 5. Подождать **DELAY_REQUEST** секунд 
Использую [@stanislavkarol/delay](https://www.npmjs.com/package/@stanislavkarol/delay)

### 7. После выхода из цикла записать считанные праздники в JSON.
```js
import fs from "fs";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

if (!fs.existsSync(`${__dirname}/../json`)) {
  fs.mkdirSync(`${__dirname}/../json`);
}

fs.writeFile(
  `${__dirname}/../json/holidays.json`,
  JSON.stringify(holidayData),
  (err) => {
    if (err) throw err;
    console.log("Data written to file");
  }
);
```

Всё вместе, в рабочем виде, на [гитхаб](https://github.com/SLKarol/get-holidays-parse).