eFusion 16 июня 2020

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

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

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() не будут работать внутри методов страницы. Контекст страницы/браузера отличается от контекста ноды, в которой работает приложение.

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

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

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

Заключение

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

Источники

РУБРИКИ В СТАТЬЕ

Комментарии 0

ВАКАНСИИ

PHP-разработчики в 8bitgroup
от 200000 RUB до 250000 RUB
Senior Backend developer (Java)
Москва, по итогам собеседования
Lead Backend Developer (Java)
по итогам собеседования

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

BUG