Парсинг веб-страниц с помощью браузера без заголовка: руководство по кукловодам

Опубликовано: 2022-03-11

В этой статье мы увидим, как легко выполнять веб-скрапинг (веб-автоматизацию) с помощью несколько нетрадиционного метода использования безголового браузера .

Что такое безголовый браузер и зачем он нужен?

За последние несколько лет Интернет эволюционировал от упрощенных веб-сайтов, созданных с использованием только HTML и CSS. Сейчас гораздо больше интерактивных веб-приложений с красивым пользовательским интерфейсом, которые часто создаются с помощью таких фреймворков, как Angular или React. Другими словами, в настоящее время JavaScript управляет Интернетом, включая почти все, с чем вы взаимодействуете на веб-сайтах.

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

Как мы упоминали выше, браузеры умеют обрабатывать JavaScript и отображать красивые веб-страницы. А что, если бы мы могли использовать эту функциональность для наших нужд в скрейпинге и имели бы способ программно управлять браузерами? Именно здесь вступает в действие автоматизация безголового браузера!

Обезглавленный? Прошу прощения? Да, это просто означает отсутствие графического пользовательского интерфейса (GUI). Вместо обычного взаимодействия с визуальными элементами, например с помощью мыши или сенсорного устройства, вы автоматизируете варианты использования с помощью интерфейса командной строки (CLI).

Безголовый Хром и Кукловод

Существует множество инструментов веб-скрейпинга, которые можно использовать для безголового просмотра, например Zombie.js или безголовый Firefox с использованием Selenium. Но сегодня мы будем исследовать безголовый Chrome через Puppeteer, так как это относительно новый проигрыватель, выпущенный в начале 2018 года. Примечание редактора: стоит упомянуть Intoli Remote Browser, еще один новый проигрыватель, но это должно стать темой для другого статья.

Что такое Кукловод? Это библиотека Node.js, которая предоставляет высокоуровневый API для управления безголовым Chrome или Chromium или для взаимодействия с протоколом DevTools. Он поддерживается командой Chrome DevTools и потрясающим сообществом с открытым исходным кодом.

Хватит разговоров — давайте перейдем к коду и узнаем, как автоматизировать просмотр веб-страниц с помощью безголового просмотра Puppeteer!

Подготовка среды

Прежде всего, на вашем компьютере должен быть установлен Node.js 8+. Вы можете установить его здесь, или, если вы любитель CLI, как и я, и хотите работать в Ubuntu, выполните следующие команды:

 curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs

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

 sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3

Настройте Headless Chrome и Puppeteer

Я бы порекомендовал установить Puppeteer с npm , так как он также будет включать стабильную актуальную версию Chromium, которая гарантированно будет работать с библиотекой.

Запустите эту команду в корневом каталоге вашего проекта:

 npm i puppeteer --save

Примечание. Это может занять некоторое время, так как Puppeteer потребуется загрузить и установить Chromium в фоновом режиме.

Хорошо, теперь, когда мы все установили и настроили, начнем самое интересное!

Использование Puppeteer API для автоматизированного парсинга веб-страниц

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

Создайте новый файл в каталоге вашего проекта с именем screenshot.js и откройте его в своем любимом редакторе кода.

Во-первых, давайте импортируем библиотеку Puppeteer в ваш скрипт:

 const puppeteer = require('puppeteer');

Далее возьмем URL из аргументов командной строки:

 const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }

Теперь нам нужно помнить, что Puppeteer — это библиотека, основанная на промисах: она выполняет асинхронные вызовы безголового экземпляра Chrome под капотом. Давайте сохраним код в чистоте, используя async/await. Для этого нам нужно сначала определить async функцию и поместить туда весь код Puppeteer:

 async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

В целом окончательный код выглядит так:

 const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();

Вы можете запустить его, выполнив следующую команду в корневом каталоге вашего проекта:

 node screenshot.js https://github.com

Подождите секунду, и бум! Наш безголовый браузер только что создал файл с именем screenshot.png , и вы можете увидеть в нем домашнюю страницу GitHub. Отлично, у нас есть работающий парсер для Chrome!

Давайте остановимся на минуту и ​​посмотрим, что происходит в нашей функции run() выше.

Сначала мы запускаем новый экземпляр безголового браузера, затем открываем новую страницу (вкладку) и переходим по URL-адресу, указанному в аргументе командной строки. Наконец, мы используем встроенный метод Puppeteer для создания снимка экрана, и нам нужно только указать путь, по которому его следует сохранить. Нам также нужно обязательно закрыть безголовый браузер после того, как мы закончим с нашей автоматизацией.

Теперь, когда мы рассмотрели основы, давайте перейдем к чему-то более сложному.

Второй пример очистки кукольника

Предположим, что в следующей части нашего руководства по Puppeteer мы хотим собрать самые свежие статьи из Hacker News.

Создайте новый файл с именем ycombinator-scraper.js и вставьте в него следующий фрагмент кода:

 const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);

Хорошо, здесь происходит немного больше по сравнению с предыдущим примером.

Первое, что вы можете заметить, это то, что функция run() теперь возвращает промис, поэтому префикс async переместился в определение функции промиса.

Мы также заключили весь наш код в блок try-catch, чтобы мы могли обрабатывать любые ошибки, которые приводят к отклонению нашего обещания.

И, наконец, мы используем встроенный метод Puppeteer, который называется evaluate() . Этот метод позволяет нам запускать пользовательский код JavaScript, как если бы мы выполняли его в консоли DevTools. Все, что возвращается этой функцией, разрешается промисом. Этот метод очень удобен, когда речь идет о очистке информации или выполнении пользовательских действий.

Код, переданный методу evaluate() , представляет собой довольно простой JavaScript, который создает массив объектов, каждый из которых имеет url -адрес и text поля, представляющие URL-адреса историй, которые мы видим на https://news.ycombinator.com/.

Вывод скрипта выглядит примерно так (но изначально с 30 записями):

 [ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]

Довольно аккуратно, я бы сказал!

Хорошо, давайте двигаться вперед. Нам вернули всего 30 товаров, а в наличии гораздо больше — они просто на других страницах. Нам нужно нажать на кнопку «Дополнительно», чтобы загрузить следующую страницу результатов.

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

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

Давайте рассмотрим, что мы сделали здесь:

  1. Мы добавили единственный аргумент с именем pagesToScrape в нашу основную функцию run() . Мы будем использовать это, чтобы ограничить количество страниц, которые будет очищать наш скрипт.
  2. Есть еще одна новая переменная с именем currentPage , которая представляет номер страницы результатов, которые мы просматриваем в данный момент. Изначально установлено значение 1 . Мы также заключили нашу функцию Assessment evaluate() в цикл while , чтобы она продолжала работать до тех пор, пока значение currentPage меньше или равно pagesToScrape .
  3. Мы добавили блок для перехода на новую страницу и ожидания загрузки страницы перед перезапуском цикла while .

Вы заметите, что мы использовали метод page.click() , чтобы безголовый браузер щелкнул кнопку «Дополнительно». Мы также использовали метод waitForSelector() , чтобы убедиться, что наша логика приостановлена ​​до загрузки содержимого страницы.

Оба они являются высокоуровневыми методами API Puppeteer, готовыми к использованию «из коробки».

Одна из проблем, с которой вы, вероятно, столкнетесь при парсинге с помощью Puppeteer, — это ожидание загрузки страницы. Hacker News имеет относительно простую структуру, и было довольно легко дождаться завершения загрузки страницы. Для более сложных случаев использования Puppeteer предлагает широкий спектр встроенных функций, которые вы можете изучить в документации по API на GitHub.

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

Оптимизация нашего сценария Puppeteer

Общая идея состоит в том, чтобы не позволять безголовому браузеру выполнять какую-либо дополнительную работу. Это может включать загрузку изображений, применение правил CSS, запуск запросов XHR и т. д.

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

В любом случае, эти оптимизации могут быть выполнены либо путем кэширования ресурсов при первом запросе, либо путем прямой отмены HTTP-запросов, когда они инициируются веб-сайтом.

Давайте сначала посмотрим, как работает кэширование.

Вы должны знать, что когда вы запускаете новый экземпляр безголового браузера, Puppeteer создает временный каталог для своего профиля. Он удаляется при закрытии браузера и недоступен для использования при запуске нового экземпляра — таким образом, все сохраненные изображения, CSS, файлы cookie и другие объекты больше не будут доступны.

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

 const browser = await puppeteer.launch({ userDataDir: './data', });

Это должно дать нам хороший прирост производительности, так как множество CSS и изображений будут кэшироваться в каталоге данных при первом запросе, и Chrome не нужно будет загружать их снова и снова.

Однако эти активы по-прежнему будут использоваться при рендеринге страницы. В наших потребностях в новостных статьях Y Combinator нам не нужно беспокоиться о каких-либо визуальных эффектах, включая изображения. Нас интересует только чистый HTML-вывод, поэтому давайте попробуем заблокировать каждый запрос.

К счастью, в этом случае с Puppeteer очень удобно работать, потому что он поддерживает пользовательские хуки. Мы можем предоставить перехватчик на каждый запрос и отменить те, которые нам действительно не нужны.

Перехватчик можно определить следующим образом:

 await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });

Как видите, у нас есть полный контроль над инициируемыми запросами. Мы можем написать собственную логику, чтобы разрешать или отменять определенные запросы в зависимости от их resourceType . У нас также есть доступ к большому количеству других данных, таких как request.url , поэтому мы можем блокировать только определенные URL-адреса, если захотим.

В приведенном выше примере мы позволяем проходить через наш фильтр только запросам с типом ресурса "document" , что означает, что мы будем блокировать все изображения, CSS и все остальное, кроме исходного ответа HTML.

Вот наш окончательный код:

 const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);

Оставайтесь в безопасности с ограничениями скорости

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

Поскольку этот аспект больше связан с архитектурой, я не буду подробно рассматривать его в этом руководстве по Puppeteer. Тем не менее, самый простой способ замедлить скрипт Puppeteer — добавить к нему команду sleep:

js await page.waitFor(5000);

Этот оператор заставит ваш скрипт спать в течение пяти секунд (5000 мс). Вы можете поместить это где угодно перед browser.close() .

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

Место кукловода в быстро развивающейся паутине

В этом руководстве по Puppeteer я продемонстрировал его основные функции в качестве инструмента для веб-скрейпинга. Тем не менее, он имеет гораздо более широкие варианты использования, включая тестирование безголового браузера, создание PDF-файлов и мониторинг производительности, среди многих других.

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