CoderCastrov logo
CoderCastrov
JavaScript - Язык программирования JavaScript

Парсинг веб-страниц с использованием JavaScript и Node.js

Парсинг веб-страниц с использованием JavaScript и Node.js
просмотров
8 мин чтение
#JavaScript - Язык программирования JavaScript

JavaScript и парсинг веб-страниц оба находятся в тренде. Мы объединим их, чтобы создать простой парсер и краулер с нуля, используя JavaScript в Node.js.

Избегание блокировок - важная часть парсинга веб-страниц, поэтому мы также добавим некоторые функции, чтобы помочь в этом. И, наконец, мы параллелизируем задачи, чтобы работать быстрее благодаря event loop Node.js.

Предварительные требования

Для работы кода вам понадобится установленный Node (или nvm) и npm. Некоторые системы уже имеют их предустановленными. После этого установите все необходимые библиотеки, запустив npm install.

npm install axios cheerio playwright

Введение

Мы используем Node v12, но вы всегда можете проверить совместимость каждой функции.

Axios - это "клиент HTTP на основе промисов", который мы будем использовать для получения HTML с URL-адреса. Он позволяет использовать различные параметры, такие как заголовки и прокси, о которых мы расскажем позже. Если вы используете TypeScript, они "включают определения TypeScript и защиту типов для ошибок Axios".

Cheerio - это "быстрая, гибкая и легкая реализация основного функционала jQuery". Он позволяет нам находить узлы с помощью селекторов, получать текст или атрибуты и многое другое. Мы передадим HTML в cheerio, а затем будем обращаться к нему, как мы делали бы в браузерной среде.

Playwright - "это библиотека Node.js для автоматизации работы с браузерами Chromium, Firefox и WebKit с помощью единого API". Когда Axios недостаточно, мы будем получать HTML с помощью безголового браузера, чтобы выполнить JavaScript и дождаться загрузки асинхронного контента.

Парсинг основ

Сначала нам понадобится HTML. Мы установили Axios для этого, и его использование просто. Мы будем использовать scrapeme.live в качестве примера, фиктивного веб-сайта, подготовленного для парсинга.

Отлично! Затем, используя cheerio, мы можем запросить две вещи, которые нам нужны прямо сейчас: ссылки пагинатора и продукты. Чтобы узнать, как это сделать, мы будем смотреть на страницу с открытыми инструментами разработчика Chrome. Все современные браузеры предлагают такие инструменты разработчика. Выберите свой любимый.

Мы отметили интересные части красным цветом, но вы можете сами попробовать. В этом случае все CSS-селекторы просты и не требуют вложенности. Проверьте руководство, если вы ищете другой результат или не можете его выбрать. Вы также можете использовать инструменты разработчика, чтобы получить селектор.

Copy Selector from DevTools

На вкладке "Elements" щелкните правой кнопкой мыши на узле ➡ Копировать ➡ Копировать селектор. Но результат обычно очень связан с HTML, как в этом случае: #main > div:nth-child(2) > nav > ul > li:nth-child(2) > a. Этот подход может стать проблемой в будущем, потому что он перестанет работать после любого минимального изменения. Кроме того, он будет захватывать только одну из ссылок пагинации, а не все.

Мы могли бы захватить все ссылки на странице, а затем отфильтровать их по содержимому. Если бы мы писали полноценный веб-краулер, это было бы правильным подходом. В нашем случае нам нужны только ссылки пагинации. Используя предоставленный класс .page-numbers a, мы захватим все ссылки и затем извлечем URL-адреса (href) из них. Селектор будет соответствовать всем узлам ссылок с предком, содержащим класс page-numbers.

Что касается продуктов (в данном случае покемонов), мы получим идентификатор, название и цену. Проверьте изображение ниже для подробностей о селекторах или попробуйте сами. Пока мы только выводим содержимое. Проверьте окончательный код для добавления их в массив.

Product (Charmander) in DevTools

Как вы можете видеть выше, все продукты содержат класс product, что упрощает нашу работу. И для каждого из них тег h2 и узел price содержат содержимое, которое нам нужно. Что касается идентификатора продукта, нам нужно сопоставить атрибут вместо класса или типа узла. Это можно сделать, используя синтаксис node[attribute="value"]. Мы ищем только узел с атрибутом, поэтому нет необходимости сопоставлять его с каким-либо конкретным значением.

Как видно из приведенного выше кода, здесь нет обработки ошибок. Мы опускаем ее для краткости во фрагментах кода, но в реальной жизни это следует учитывать. В большинстве случаев возврат значения по умолчанию (т.е. пустой массив) должен сработать.

Следование ссылкам

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

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

Для следующей части мы будем использовать async/await, чтобы избежать обратных вызовов и вложенности. Асинхронная функция - это альтернатива цепочке функций, основанных на промисах. В этом случае вызов Axios останется асинхронным. Это может занять около 1 секунды на страницу, но мы пишем код последовательно, без необходимости использования обратных вызовов.

Есть небольшая особенность: await допустим только в асинхронной функции. Это заставит нас обернуть исходный код в функцию, конкретно в IIFE (Immediately Invoked Function Expression). Синтаксис немного странный. Он создает функцию, а затем сразу вызывает ее.

Избегайте блокировок

Как уже упоминалось ранее, нам нужны механизмы, чтобы избежать блокировок, капч, стен авторизации и других защитных техник. Сложно предотвратить их на 100% времени. Но мы можем достичь высокой степени успеха с помощью простых усилий. Мы применим две тактики: добавление прокси-серверов и полного набора заголовков.

Существуют бесплатные прокси-серверы, хотя мы не рекомендуем их использовать. Они могут работать для тестирования, но они ненадежны. Мы можем использовать некоторые из них для тестирования, как мы увидим в некоторых примерах. Обратите внимание, что эти бесплатные прокси-серверы могут не работать для вас. Они имеют ограниченное время жизни.

Платные прокси-сервисы, с другой стороны, предлагают вращение IP-адресов. Это означает, что наш сервис будет работать так же, но целевой веб-сайт будет видеть другой IP-адрес. В некоторых случаях они вращаются для каждого запроса или каждые несколько минут. В любом случае, их гораздо сложнее заблокировать. И когда это происходит, мы получим новый IP-адрес через некоторое время.

Мы будем использовать httpbin для тестирования. Он предлагает несколько конечных точек, которые будут отвечать заголовками, IP-адресами и многими другими.

Следующим шагом будет проверка заголовков нашего запроса. Самый известный из них - User-Agent (UA для краткости), но есть и многие другие. Многие программные инструменты имеют свои собственные, например, Axios (axios/0.21.1). В целом, хорошей практикой является отправка фактических заголовков вместе с UA. Это означает, что нам нужен набор реальных заголовков, потому что не все браузеры и версии используют одни и те же. Мы включаем два в примере: Chrome 92 и Firefox 90 на машине с Linux.

Proxy

Браузеры без графического интерфейса

До сих пор каждая посещенная страница была получена с помощью axios.get, что может быть недостаточным в некоторых случаях. Допустим, нам нужно загрузить и выполнить JavaScript или взаимодействовать с браузером (с помощью мыши или клавиатуры). Хотя избегать этого желательно из-за проблем с производительностью, иногда нет другого выбора. Selenium, Puppeteer и Playwright - самые используемые и известные библиотеки. В приведенном ниже фрагменте показан только User-Agent, но поскольку это настоящий браузер, заголовки будут включать весь набор (Accept, Accept-Encoding и т. д.).

Этот подход имеет свои проблемы: посмотрите на User-Agent. В User-Agent Chromium есть "HeadlessChrome", что скажет целевому веб-сайту, что это браузер без графического интерфейса. Они могут реагировать на это.

Как и с Axios, мы можем предоставить дополнительные заголовки, прокси и множество других параметров для настройки каждого запроса. Отличный выбор, чтобы скрыть наш User-Agent "HeadlessChrome". И поскольку это настоящий браузер, мы можем перехватывать запросы, блокировать другие (например, файлы CSS или изображения), делать снимки экрана или видео и многое другое.

Теперь мы можем разделить получение HTML на несколько функций: одну с использованием Playwright и другую с использованием Axios. Затем нам потребуется способ выбрать подходящий вариант для каждого конкретного случая. Пока что это захардкодено. Кстати, вывод одинаковый, но гораздо быстрее при использовании Axios.

Headless Browsers

Использование асинхронности в JavaScript

Мы уже познакомились с async/await при парсинге нескольких ссылок последовательно. Если мы хотим парсить их параллельно, достаточно просто удалить await, верно? Но... не так быстро.

Функция вызывает первый crawl и сразу же берет следующий элемент из набора toVisit. Проблема в том, что набор пуст, так как парсинг первой страницы еще не произошел. Мы не добавили новые ссылки в список. Функция продолжает работать в фоновом режиме, но мы уже вышли из основной функции.

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

Если вы запустите приведенный выше код, он немедленно выведет числа от 0 до 3 (со временной меткой), а от 4 до 7 через 2 секунды. Это, возможно, самый сложный отрывок для понимания - изучите его без спешки.

Мы определяем queue в строках 1-20. Он вернет объект с функцией enqueue, чтобы добавить задачу в список. Затем он проверяет, превышаем ли мы лимит параллельности. Если нет, он увеличит running на единицу и войдет в цикл, который получает задачу и выполняет ее с предоставленными параметрами. Пока список задач не пуст, затем вычтет единицу из running. Эта переменная указывает, когда мы можем или не можем выполнять еще задачи, разрешая это только при условии, что количество задач не превышает лимит параллельности. В строках 23-28 находятся вспомогательные функции sleep и printer. Создаем экземпляр очереди в строке 30 и добавляем задачи в очередь в строках 32-34 (которые начнут выполняться параллельно).

Теперь мы должны использовать очередь вместо цикла for для парсинга нескольких страниц параллельно. Ниже приведен частичный код с изменениями.

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

Чтобы продвинуться дальше, нам нужно использовать хранилище (базу данных) или распределенную систему очередей. В настоящее время мы полагаемся на переменные, которые не являются общими для потоков в Node. Это не слишком сложно, но мы уже достаточно охватили в этом блог-посте.

Итоговый код

Заключение

Мы можем создать пользовательский веб-парсер с использованием JavaScript и Node.js, используя рассмотренные компоненты. Возможно, он не масштабируется до тысяч веб-сайтов, но отлично работает для нескольких. И переход к распределенному парсингу не настолько далек.

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

Спасибо за чтение. Нашли ли вы полезный контент? Пожалуйста, поделитесь им. 👇


Опубликовано на https://www.zenrows.com