🕵 Puppeteer: парсинг сайтов с JavaScript
Бывает, что прежде чем получить данные парсинга веб-сайта, необходимо выполнить ряд действий на странице. Библиотека Puppeteer позволяет создавать веб-скраперы, имитирующие действия пользователя.
Puppeteer: не просто очередная библиотека для парсинга
Puppeteer – это библиотека Node.js, поддерживаемая командой Chrome Devtools. Библиотека запускает экземпляр Chrome/Chromium и предоставляет набор высокоуровневых API.
Puppeteer используется для выполнения множества различных задач:
- автоматизация сбора данных с веб-сайтов;
- создание скриншотов и PDF-файлов;
- тестирование расширений Chrome;
- автоматизация тестирования веб-интерфейсов;
- диагностика проблем производительности с помощью таких методов, как захват временной шкалы трассировки веб-сайта.
В сравнении с Selenium библиотека Puppeteer не обладает кросс-браузерностью, но часто выигрывает в скорости, так как не имеет промежуточного звена в виде Selenium server – команды идут напрямую в браузер.
В этой статье мы рассмотрим пример сбора данных с одной из страниц Amazon со списком товаров. Извлечем информацию из страницы списка лучших рубашек, поместим в JSON и посмотрим, как эмулировать действия пользователя (поиск товара). Полный код проекта доступен в репозитории.
Установка 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 мс.
Собираем данные со страницы
Выясним, какие данные необходимо извлечь. Нас интересует бренд, название продукта, ссылки на продукт и его изображение, и, наконец, стоимость:
{ 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. Для лучшего понимания возможностей инструмента рекомендуем потратить некоторое время на ознакомление с официальной документацией. Многое о библиотеке можно понять из оглавления, содержащего перечень классов и методов.