Подробное руководство по созданию минимального и надежного веб-скребка для извлечения структурированных данных из Интернета.

Node.js предоставляет идеальную динамическую среду для быстрого экспериментирования и работы с данными из Интернета.

Хотя в наши дни появляется все больше и больше продуктов для визуального соскабливания (import.io, Spider, Scrapinghub, Apify, Crawly, ……), всегда будет потребность в простоте и гибкости списание одноразовых скребков вручную.

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

В частности, мы рассмотрим, как создать парсер для списка популярных репозиториев на GitHub. Если вы хотите следить за кодом, посмотрите репозиторий scrape-github-trending.

Строительные блоки

Одна из лучших особенностей Node.js - это чрезвычайно обширное сообщество модулей с открытым исходным кодом, которые он может предложить. Для этого типа задач мы будем сильно полагаться на два модуля: got для надежной загрузки необработанного HTML и cheerio, который предоставляет API на основе jQuery. для разбора и просмотра этих страниц.

Cheerio действительно отлично подходит для быстрого и грязного парсинга веб-страниц, когда вы просто хотите работать с необработанным HTML. Если вы имеете дело с более сложными сценариями, в которых вы хотите, чтобы ваш сканер имитировал как можно больше реального пользователя или выполнял клиентские скрипты, вы, вероятно, захотите использовать Puppeteer.

В отличие от cheerio, puppeteer - это оболочка для автоматизации экземпляров Chrome без головы, что действительно полезно для работы с современными SPA на основе JS. Поскольку вы работаете с самим Chrome, он также имеет лучшую в своем классе поддержку для соответствия синтаксическому анализу / рендерингу / скриптингу. Headless Chrome все еще относительно новый, но в ближайшие годы он, вероятно, постепенно откажется от старых подходов, таких как PhantomJS.

Что касается got, то на NPM доступны десятки библиотек HTTP-выборки, причем некоторые из наиболее популярных альтернатив - суперагент, axios, unetch ​​(изоморфный === можно использовать из Node.js или браузера), и, наконец, request / request-prom-native (самая популярная библиотека на сегодняшний день, хотя разработчики официально отказались от любых будущих разработок).

Начиная

Хорошо, для этого урока мы напишем парсер для списка популярных репозиториев GitHub.

Первое, что я делаю при написании парсера, - это открываю целевую страницу в Chrome и смотрю, как желаемые данные структурированы в инструментах разработчика.

Переключаясь между вкладками Console и Elements, вы можете использовать селектор $$(‘.repo-list li’) в консоли, чтобы выбрать все репозитории с тенденциями.

При создании этих CSS-селекторов вы хотите, чтобы они были как можно более простыми, но в то же время как можно более сфокусированными. Просматривая вкладку Elements и выбирая интересующие вас элементы, вы обычно обнаруживаете несколько потенциальных селекторов, которые могут сработать. Следующий шаг - опробовать их на вкладке Console, используя синтаксис $$(), чтобы убедиться, что вы выбираете только те элементы, которые собирались выбрать. Одно из практических правил здесь - стараться избегать использования аспектов структуры или классов HTML, которые могут чаще изменяться при рефакторинге или переписывании кода.

Напишем скребок!

Теперь, когда у нас есть хорошее представление о некоторых селекторах CSS, которые будут нацелены на наши желаемые данные, давайте преобразуем их в сценарий Node.js:

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

  • Строка 12: мы загружаем удаленную страницу и извлекаем из нее текст body (HTML).
  • Строка 14: мы загружаем этот HTML-код в cheerio, чтобы его было легко перемещать и манипулировать.
  • Строка 15: мы выбираем все li элементы репозитория, используя наш предыдущий селектор CSS, и сопоставляем их.
  • Строки 16–32: ​​мы извлекаем соответствующие части каждого трендового репо в простой объект JSON.
  • Строка 33: здесь мы отфильтровываем все репозитории, которые не удалось правильно проанализировать или выдать ошибку. Это будет undefined в массиве, а [].filter(Boolean) - это сокращенный синтаксис для фильтрации любых неверных значений.

На данный момент нам удалось очистить одну веб-страницу и извлечь некоторые релевантные данные. Вот пример вывода JSON на этом этапе:

Ползать глубже

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

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

Когда у вас будет хорошее представление о том, какие данные вы хотите извлечь, и у вас будет несколько рабочих селекторов в Console, пора написать функцию Node.js для загрузки и анализа одного репозитория GitHub.

Единственное реальное отличие здесь от нашего первого примера очистки состоит в том, что мы используем несколько разных cheerio служебных методов, например $ .find (), а также выполняем дополнительный синтаксический анализ строк для приведения данных в соответствие с нашими потребностями.

На данный момент мы можем извлечь множество наиболее полезных метаданных о каждом репо в отдельности, но нам нужен способ надежного сопоставления всех репозиториев, которые мы хотим обработать. Для этого воспользуемся прекрасным модулем p-map. В большинстве случаев вы хотите установить практический предел параллелизма, будь то регулирование пропускной способности сети или вычислительных ресурсов. Вот где действительно сияет p-map. Я использую его в 99% случаев как замену для Promise.all(…), который не поддерживает ограничение параллелизма.

Здесь мы сопоставляем каждый репозиторий с максимальным параллелизмом 3 запроса за раз. Это значительно помогает сделать ваш поисковый робот более устойчивым к случайным неполадкам в сети и на сервере.

Если вы хотите добавить здесь еще один уровень надежности, я бы рекомендовал обернуть ваши асинхронные функции вспомогательной очистки в p-retry и p-timeout. Это то, что на самом деле делает got для обеспечения более надежных HTTP-запросов.

Все вместе сейчас

Вот полный исполняемый код Node.js. Вы также можете найти полностью воспроизводимый проект на сайте scrape-github-trending.

И пример соответствующего вывода JSON:

Заключение

Я использовал этот точный шаблон десятки раз для разовых задач очистки в Node.js. Он прост, надежен и действительно легко настраивается практически под любые целевые сценарии сканирования / очистки.

Стоит отметить, что scrape-it также выглядит как очень хорошо спроектированная библиотека, которая, по сути, делает все, что описано в этой статье.

Если ваш вариант использования сканирования требует более распределенного рабочего процесса или более сложного анализа на стороне клиента, я настоятельно рекомендую проверить Puppeteer, библиотеку, меняющую правила игры от Google для автоматизации Chrome без браузера. Вы также можете проверить связанные ресурсы сканирования, перечисленные в awesome-puppeteer, такие как headless-chrome-crawler, который предоставляет решение для распределенного сканирования, построенное на основе Puppeteer.

Однако по моему опыту, в 95% случаев простой однофайловый сценарий, подобный тому, что описан в этой статье, как правило, отлично справляется со своей задачей. И имхо, KISS - это самое важное правило в разработке программного обеспечения.

Спасибо за уделенное время и желаю удачи в ваших будущих приключениях со скребками!

Прежде чем ты уйдешь…

Если вам понравилась эта статья, нажмите 👏 ниже, пометьте репо или поделитесь ею с другими, чтобы они тоже могли ею насладиться.