Пишем парсер-мониторинг для «Hyundai Showroom» с выгрузкой в телеграм-канал

от автора

На сайте https://showroom.hyundai.ru/ можно заказать машину без переплат, напрямую с завода Hyundai, но проблема в том, что машины уходят очень быстро. При этом новые автомобили появляются нечасто, и, чаще всего, можно наблюдать на сайте сообщение об отсутствии машин.

Чтобы успеть забронировать машину, напишем парсер-мониторинг для «Hyundai Showroom» с выгрузкой в телеграм-канал, который будет уведомлять о том, появились ли машины в шоуруме.

Будем использовать язык JavaScript, окружение Node.js, и следующие библиотеки:

  • puppeteer для программного управления браузером;

  • node-telegram-bot-api для отправки сообщения в телеграм-канал;

  • node-cron для установки запуска скрипта по расписанию;

  • winston для логирования.

Заведем константы, в которых опишем хост сайта шоурума Hyundai, доступы для телеграм-канала и переменную окружения:

const hyundaiHost = 'https://showroom.hyundai.ru/'; const tgToken = 'SOME_TELEGRAM_TOKEN'; const tgChannelId = 'SOME_TELEGRAM_CHANNEL_ID'; const isProduction = process.env.NODE_ENV === 'production';

Создадим новые инстансы модулей телеграм-бота и логгера.

Логгер нужен для того, чтобы сохранить в файловой системе информацию о данных, которые получил парсер, когда загрузил страницу. Это может помочь при отладке и, например, будет полезно для сравнения работы парсера с другими парсерами:

const bot = new TelegramBot(tgToken); const logger = winston.createLogger({     transports: [         new winston.transports.File({             filename: './log.txt',         }),     ], });

Функция start запускает функцию exec и устанавливает cron. Функция exec содержит основную часть бизнес-логики скрипта:

async function start() {     exec();      cron.schedule('* * * * *', () => {         exec();     }); }

Опишем функцию exec.

Создадим инстанс браузера в режиме headless, чтобы в операционной системе не запускался графический интерфейс браузера. Пропишем дополнительные аргументы, которые позволят ускорить работу браузера:

  const browser = await puppeteer.launch({     headless: true,     args: [         '--disable-gpu',         '--disable-dev-shm-usage',         '--disable-setuid-sandbox',         '--no-first-run',         '--no-sandbox',         '--no-zygote',     ], });

Создадим новую страницу, а также вызовем функцию setBlockingOnRequests, — эта функция установит блокировку некоторых сетевых запросов, которые происходят на странице шоурума. Это нужно чтобы ресурсы, не относящиеся к полезной работе парсера, не загружались. Например, изображения или сторонние скрипты, такие как Google-аналитика и рекламные системы:

const page = await browser.newPage();  await setBlockingOnRequests(page);

Сделаем первый вызов try-catch, в котором загрузим страницу. Если страница не загрузилась, создадим отчет об ошибке при помощи функции createErrorReport. Передадим туда аргументы:

  • инстанс страницы браузера;

  • идентификатор no-page;

  • сообщение «Ошибка посещения страницы»;

  • системную ошибку.

После этого закроем страницу браузера и выйдем из функции exec:

  try {     await page.goto(hyundaiHost, {waitUntil: 'networkidle2'}); } catch (error) {     await createErrorReport(page, 'no-page', 'Ошибка посещения страницы', error);      await page.close();     await browser.close();     return; }

Если страница успешно загрузилась, сделаем следующий вызов try-catch, где попробуем найти CSS-селектор '#cars-all .car-columns' в DOM – так узнаем, отображается ли на странице список автомобилей или нет:

await page.waitForSelector('#cars-all .car-columns', {timeout: 1000});

Также посчитаем количество машин по количеству вхождений в DOM CSS-селектора, принадлежащего к карточке автомобиля:

const carsCount = (await page.$$('.car-item__wrap')).length;

Сформулируем временную метку и сообщение, которое затем отправим в телеграм-канал. Будем использовать функцию pluralize, которая подберет правильное склонение слова в зависимости от числительного:

const timestamp = new Date().toTimeString(); const message = `${pluralize(carsCount, 'Доступна', 'доступно', 'доступно')} ${carsCount} ${pluralize(carsCount, 'машина', 'машины', 'машин')} в ${timestamp}`;

Если приложение запущено в боевой среде, отправим сообщение в телеграм-канал:

if (isProduction) {     bot.sendMessage(tgChannelId, message); }

Если CSS-селектор списка машин не найден в DOM, создадим сообщение об ошибке, а затем завершим сессию страницы и браузера:

await createErrorReport(page, 'no-cars', 'Ошибка поиска машин', error); await page.close(); await browser.close();

Разберем функцию createErrorReport. Формируем сообщения для записи в файл лога:

const timestamp = new Date().toTimeString();  logger.error(`${message} в ${timestamp}`, techError);

Создадим скриншот средствами puppeteer чтобы убедиться, действительно ли машины отсутствовали или, например, изменилась верстка сайта и CSS-селекторы, на которые мы ориентируемся, потеряли актуальность.

Установим самое низкое качество изображения, чтобы файл получился минимального размера, и чтобы большое количество скриншотов не загружали дисковое пространство:

const carListContainer = await page.$('#main-content');  if (carListContainer) {     await carListContainer.screenshot({path: `${type}-${timestamp}.jpeg`, type: 'jpeg', quality: 1}); } else {     logger.error(`Не могу сделать скриншот отсутствия автомобилей в ${timestamp}`, techError); }

Рассмотрим функцию setBlockingOnRequests, которая включает режим перехвата запросов для страницы в puppeteer и устанавливает обработчик события.

Далее, при помощи геттеров resourceType и url, проверим тип и URL загружаемого ресурса. Заблокируем картинки, медиа-файлы, шрифты, CSS-файлы, системы веб-аналитики и рекламные системы, так как никакой полезной информации для парсинга они не несут.

async function setBlockingOnRequests(page) {     await page.setRequestInterception(true);      page.on('request', (req) => {         if (req.resourceType() === 'image'             || req.resourceType() === 'media'             || req.resourceType() === 'font'             || req.resourceType() === 'stylesheet'             || req.url().includes('yandex')             || req.url().includes('nr-data')             || req.url().includes('rambler')             || req.url().includes('criteo')             || req.url().includes('adhigh')             || req.url().includes('dadata')         ) {             req.abort();         } else {             req.continue();         }     }); }

Функция pluralize:

function pluralize(n, one, few, many) {     const selectedRule = new Intl.PluralRules('ru-RU').select(n);      switch (selectedRule) {         case 'one': {             return one;         }         case 'few': {             return few;         }         default: {             return many;         }     } }

Основное преимущество подобного метода парсинга — несложная реализация, но имеется недостаток — недостаточная надежность, как следствие нестабильной работы сайта шоурума. Его можно исправить, перейдя к работе с REST API, с которым работает сайт шоурума — https://showroom.hyundai.ru/rest/car, но тут мы встретим новое препятствие — шифрование данных.


ссылка на оригинал статьи https://habr.com/ru/articles/593819/