eFusion 16 июня 2020

🕵 Puppeteer: парсинг сайтов с JavaScript

Бывает, что прежде чем получить данные парсинга веб-сайта, необходимо выполнить ряд действий на странице. Библиотека Puppeteer позволяет создавать веб-скраперы, имитирующие действия пользователя.
🕵 Puppeteer: парсинг сайтов с JavaScript

Puppeteer: не просто очередная библиотека для парсинга

Puppeteer – это библиотека Node.js, поддерживаемая командой Chrome Devtools. Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор высокоуровневых API.

Puppeteer используется для выполнения множества различных задач:

  • автоматизация сбора данных с веб-сайтов;
  • создание скриншотов и PDF-файлов;
  • тестирование расширений Chrome;
  • автоматизация тестирования веб-интерфейсов;
  • диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.

В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.

В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.

Исследуемая страница товаров Amazon
Исследуемая страница товаров Amazon

Установка Puppeteer и навигация

Puppeteer без проблем устанавливается с помощью npm:

        npm install --save puppeteer
    

Создадим экземпляр браузера и страницы, перейдем к целевому URL-адресу:

        const puppeteer = require('puppeteer');

const url = 'https://www.amazon.in/s?k=Shirts&ref=nb_sb_noss_2';

async function fetchProductList(url) {
    const browser = await puppeteer.launch({ 
        headless: true, // false: enables one to view the Chrome instance in action
        defaultViewport: null, // (optional) useful only in non-headless mode
    });
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    ...
}

fetchProductList(url);
    

Назначение экземпляров интуитивно понятно:

  • browser: запускает экземпляр Chrome при вызове puppeteer.launch. Простая эмуляция браузера.
  • page: напоминает одну вкладку в браузере Chrome. Предоставляет набор методов, которые можно применить к конкретному экземпляру страницы/ Вызывается при запуске browser.newPage. Как в браузере можно создать несколько вкладок, так в Puppeteer можно одновременно обрабатывать несколько экземпляров страниц.

В качестве значения параметра waitUntil используем networkidle2 . Это гарантирует, что состояние загрузки страницы считается окончательным, если она имеет не более 2 подключений, работающих в течение не менее 500 мс.

Собираем данные со страницы

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

Требуемая структура блока JSON
        {
    brand: 'Brand Name', 
    product: 'Product Name',
    url: 'https://www.amazon.in/url.of.product.com/',
    image: 'https://www.amazon.in/image.jpg',
    price: '₹599',
}
    

Для запроса DOM используем метод page.evaluate() . Для обхода DOM – обычные методы JavaScript document.querySelector и document.querySelectorAll.

        async function fetchProductList(url) {
	...
    
    await page.waitFor('div[data-cel-widget^="search_result_"]');

    const result = await page.evaluate(() => {
        // counts total number of products
        let totalSearchResults = Array.from(document.querySelectorAll('div[data-cel-widget^="search_result_"]')).length;

        let productsList = [];

        for (let i = 1; i < totalSearchResults - 1; i++) {
            let product = {
                brand: '',
                product: '',
            };
            let onlyProduct = false;
            let emptyProductMeta = false;
			
            // traverse for brand and product names
            let productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base`));

            if (productNodes.length === 0) {
                // traverse for brand and product names 
				// (in case previous traversal returned empty elements)
                productNodes = Array.from(document.querySelectorAll(`div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal`));
                productNodes.length > 0 ? onlyProduct = true : emptyProductMeta = true;
            }

            let productsDetails = productNodes.map(el => el.innerText);

            if (!emptyProductMeta) {
                product.brand = onlyProduct ? '' : productsDetails[0];
                product.product = onlyProduct ? productsDetails[0] : productsDetails[1];
            }
			
            // traverse for product image
            let rawImage = document.querySelector(`div[data-cel-widget="search_result_${i}"] .s-image`);
            product.image =rawImage ? rawImage.src : '';
			
            // traverse for product url
            let rawUrl = document.querySelector(`div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal`);
            product.url = rawUrl ? rawUrl.href : '';

            // traverse for product price
            let rawPrice = document.querySelector(`div[data-cel-widget="search_result_${i}"] span.a-offscreen`);
            product.price = rawPrice ? rawPrice.innerText : '';

            if (typeof product.product !== 'undefined') {
                !product.product.trim() ? null : productsList = productsList.concat(product);
            }
        }

        return productsList;
    });
    
    ...
}
    
...
    

После изучения DOM стало ясно, что каждый перечисленный элемент выводится с селектором div[data-cel-widget^="search_result_"]. Данный селектор ищет все теги div с атрибутом data-cel-widget, которые имеют значение, начинающееся с search_result_. Аналогичным образом исключаем селекторы со следующими параметрами:

  • total listed items: div[data-cel-widget^="search_result_"]
  • brand: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base (i обозначает номер узла в total listed items)
  • product: div[data-cel-widget="search_result_${i}"] .a-size-base-plus.a-color-base или div[data-cel-widget="search_result_${i}"] .a-size-medium.a-color-base.a-text-normal
  • url: div[data-cel-widget="search_result_${i}"] a[target="_blank"].a-link-normal
  • image: div[data-cel-widget="search_result_${i}"] .s-image
  • price: div[data-cel-widget="search_result_${i}"] span.a-offscreen

Примечание: мы ожидаем доступа к селектору именованных элементов div [data-cel-widget^="search_result_"] с помощью метода page.waitFor.

При запуске метода page.evaluate мы увидим в логе необходимые данные:

Логи запущенного метода
Логи запущенного метода

Имитируем поведение пользователя

Настройка автоматизации
Настройка автоматизации

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

Но что если, прежде чем извлечь данные, мы должны перейти по нескольким URL? Puppeteer умеет имитировать поведение юзера. Перейдем на домашнюю страницу amazon.in и найдем рубашки. Это приведет нас к странице списка продуктов, а оттуда мы сможем извлечь необходимые данные из DOM. Взглянем на код:

        ...

async function fetchProductList(url, searchTerm) {
	...
	await page.goto(url, { waitUntil: 'networkidle2' });

    await page.waitFor('input[name="field-keywords"]');
    await page.evaluate(val => document.querySelector('input[name="field-keywords"]').value = val, searchTerm);

    await page.click('div.nav-search-submit.nav-sprite');
    
    ...
}

fetchProductList('https://amazon.in', 'Shirts');
    

Ждем, пока поле поиска будет доступно, а затем добавляем searchTerm, переданный с помощью page.evaluate. Переходим на страницу списка продуктов, эмулируя щелчок по кнопке поиска – получаем желанное.

Некоторые нюансы в работе

Есть несколько моментов, с которыми вы можете столкнуться во время работы.

  • Некоторые ресурсы могут заблокировать доступ, если заподозрят странную активность. Для рандомизации user-agent в браузере используйте пакет user-agents:
        const puppeteer = require('puppeteer');
const userAgent = require('user-agents');

...

const browser = await puppeteer.launch({ headless: true, defaultViewport: null });
const page = await browser.newPage();
await page.setUserAgent(userAgent.toString());

...
    
  • Puppeteer не идеален в вопросе производительности. Повысить эффективность можно за счет троттлинга анимации, ограничения сетевых вызовов и т. д.
  • Не забывайте завершать сеанс Puppeteer, закрывая экземпляр браузера с помощью browser.close.
  • Некоторые распространенные операции, такие как console.log() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

Собираем всё вместе

Автоматизируем навигацию на странице со списком продуктов.

🕵 Puppeteer: парсинг сайтов с JavaScript

Теперь у вас есть собственный и настраиваемый API для сбора данных. Остается лишь подключить это все к серверной платформе.

Заключение

Мы рассмотрели всего лишь один конкретный случай использования Puppeteer. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.

Больше полезной информации вы можете найти на канале «Библиотека фронтендера».

Источники

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
PHP Developer
от 200000 RUB до 270000 RUB
Golang разработчик (middle)
от 230000 RUB до 300000 RUB

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ