DEV Community

Stanislav Karol
Stanislav Karol

Posted on • Updated on

Написал парсер сайта на Node.js

По прошлым публикациям видно, что у меня есть некий телеграм-бот. Этот бот имеет такую фичу: поздравляет с каким-нибудь праздником на этот день. Всякий раз, когда нужно было вызвать эту фичу, он идёт на сайт со списком праздников, берёт заданную дату и выводит название праздника. Настало время, когда такая зависимость от сайта меня перестала устраивать. Решено забрать от сайта все праздники, записать их куда-нибудь по-ближе.
Итак алгоритм этой работы будет таким:

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

Перейдём к реализации на 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);
}
Enter fullscreen mode Exit fullscreen mode

2. Сформировать список ссылок для первых COUNT_REQUEST дней

/**
 * @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;
}
Enter fullscreen mode Exit fullscreen mode

3. Запросить праздники для первых COUNT_REQUEST дней

Для этого понадобятся два пакета node-fetch и node-html-parser.
Для реализации использовал фичу из 16 версии nodejs AbortController . Хотя и не полностью как в статье сделал,- setTimeout у меня по старинке запускается.

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))
    );
Enter fullscreen mode Exit fullscreen mode

4. Запомнить праздники

Результат собирается в массив

let holidayData = [];
///
const promisesOfHolidays = await Promise.all(
    urlsData.map(async (ud) => await getHolydays(ud.url, ud.date))
    );
holidayData = [
      ...holidayData,
      ...promisesOfHolidays.filter((r) => r !== null),
    ];
Enter fullscreen mode Exit fullscreen mode

5. Подождать DELAY_REQUEST секунд

Использую @stanislavkarol/delay

7. После выхода из цикла записать считанные праздники в JSON.

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");
  }
);
Enter fullscreen mode Exit fullscreen mode

Всё вместе, в рабочем виде, на гитхаб.

Discussion (0)