Подробное руководство по созданию минимального и надежного веб-скребка для извлечения структурированных данных из Интернета.
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 - это самое важное правило в разработке программного обеспечения.
Спасибо за уделенное время и желаю удачи в ваших будущих приключениях со скребками!
Прежде чем ты уйдешь…
Если вам понравилась эта статья, нажмите 👏 ниже, пометьте репо или поделитесь ею с другими, чтобы они тоже могли ею насладиться.