DEV Community

Stanislav Karol
Stanislav Karol

Posted on • Edited on

3 2

Написал парсер сайта на 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

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

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay