Frontender Magazine

«Грабим» с помощью Node.js

«Сграбить» — программно получить данные из интернета. С ростом объёмов данных «грабёж» становится всё более распространённым явлением, и для упрощения процесса возникло множество мощных сервисов.

Иллюстрация

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

В этой статье мы рассмотрим:

Что ещё стоит иметь в виду: для понимания этой статьи рекомендуется иметь базовое представление о Node.js, так что если вы вообще не в теме, посмотрите это, прежде чем продолжить. Кроме того, пользовательское соглашение некоторых сайтов запрещает их «грабить», так что стоит прояснить этот момент, прежде чем что-либо делать.

Модули

Чтобы установить Node.js-модули, о которых говорилось выше, мы будем использовать NPM — Node Package Manager. Если вы слышали о Bower — это то же самое, за тем исключением, что NPM используется для установки Bower. NPM — это менеджер пакетов, который автоматически устанавливается вместе с Node.js, чтобы сделать использование пакетов максимально простым. По умолчанию NPM устанавливает модули в папку node_modules в той же директории, в которой была выполнена команда, так что проверьте, что команда установки выполняется в папке проекта.

Вот модули, которые мы будем использовать.

Request

Node.js предоставляет несколько простых методов скачивания данных из интернета через HTTP и HTTPS, но их нужно обрабатывать отдельно, не говоря уже о редиректах и других задачах, которые возникают в процессе «грабежа». Модуль Request объединяет эти методы, позволяя абстрагироваться от рутины, и предоставляет интерфейс для создания запросов. Мы будем использовать этот модуль, чтобы скачивать страницы непосредственно в память. Чтобы его установить, выполните в терминале команду npm install request в директории, где будет находиться основной Node.js-скрипт.

Cheerio

Cheerio позволяет работать со скачанными из сети данными, используя синтаксис, аналогичный jQuery. Процитирую текст с главной страницы проекта: «Cheerio — это быстрый, гибкий и надёжный порт jQuery, разработанный специально для сервера». Использование Cheerio позволяет сконцентрироваться непосредственно на работе с полученными данными, а не на их парсинге. Для установки выполните в терминале команду npm install cheerio в директории, в которой будет находиться основной Node.js-скрипт.

Использование

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

var request = require("request"),
    cheerio = require("cheerio"),
    url = "http://www.wunderground.com/cgi-bin/findweather/getForecast?&query=" + 02888;

request(url, function (error, response, body) {
    if (!error) {
        var $ = cheerio.load(body),
            temperature = $("[data-variable='temperature'] .wx-value").html();

        console.log("Температура " + temperature + " градусов по Фаренгейту.");
    } else {
        console.log("Произошла ошибка: " + error);
    }
});

Так, и что мы тут делаем? Сначала подключаем модули, которые будем использовать, затем сохраняем в переменной URL, содержимое которого собираемся «сграбить».

После этого используем модуль Request, чтобы скачать страницу, находящуюся по URL, переданному функции request. Мы передаём в качестве аргументов URL, содержимое которого хотим скачать, и колбек, который обработает результаты запроса. Когда мы получим данные, будет вызван колбек, в который в качестве аргументов будут переданы три переменные: error, response и body. Если Request не сможет скачать страницу и получить данные, он передаст в функцию объект в переменной error и null, в качестве аргумента body. Прежде чем начинать работать с данными, проверяем не произошла ли ошибка; если произошла — выводим сообщение в консоль, чтобы увидеть, что именно пошло не так.

Если всё хорошо, передаём Cheerio полученные данные. В результате мы можем работать с данными как на обычном сайте, используя стандартный синтаксис jQuery. Чтобы найти на странице интересующие нас данные, надо написать селектор, который получит нужный элемент (или набор элементов). Если вы откроете в браузере URL из примера и исследуете страницу с помощью инструментов разработчика, вы увидите, что большой зелёный блок с температурой на нём — тот самый элемент, для которого написан селектор. Теперь, когда мы его нашли, осталось просто получить из него данные и вывести их в консоль.

Отсюда можно двигаться по нескольким направлениям. Например, поиграйте с кодом, я привёл основные шаги ниже:

В браузере

  1. Откройте страницу, которую хотите «сграбить», сохраните URL.
  2. Определите элемент или элементы, данные из которых вы хотите получить, и jQuery-селектор, который позволяет получить эти элементы.

В коде

  1. Используйте Request, чтобы скачать страницу, находящуюся по выбранному вами URL.
  2. Передайте полученные данные Cheerio, чтобы использовать jQuery-подобный интерфейс.
  3. Используйте селектор, который вы написали перед этим, чтобы «сграбить» данные со страницы.

Идём дальше: дата-майнинг

Более продвинутое использование грабберов можно отнести к дата-майнингу — процессу скачивания страниц и генерации отчётов на основе полученных данных. Node.js отлично подходит для создания подобных приложений.

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

В приложении есть три главных шага:

  1. Проанализировать поисковую выдачу Google.
  2. Скачать все страницы и распарсить текст на каждой из них.
  3. Проанализировать текст и предоставить статистику по наиболее популярным словам.

Давайте посмотрим на код, который позволит реализовать каждый из этих шагов — как вы можете догадаться, его совсем немного.

Скачиваем поисковую выдачу Google

Первое, что нужно сделать — определить какую страницу мы собираемся анализировать. Так как мы собираемся анализировать страницы из поисковой выдачи Google, просто используем URL с нужным поисковым запросом, скачиваем и парсим результаты, чтобы получить URL нужных нам страниц.

Чтобы скачать страницы, используем Request как в примере выше, и парсим полученные данные используя Cheerio. Вот как выглядит код:

request(url, function (error, response, body) {
    if (error) {
        console.log(“Не удалось получить страницу из за следующей ошибки: “ + error);
        return;
    }

  // загружаем тело страницы в Cheerio чтобы можно было работать с DOM
    var $ = cheerio.load(body),
        links = $(".r a");

    links.each(function (i, link) {
    // получаем атрибуты href для каждой ссылки
        var url = $(link).attr("href");

    // обрезаем ненужный мусор
        url = url.replace("/url?q=", "").split("&")[0];

        if (url.charAt(0) === "/") {
            return;
        }

    // ссылка считается результатом, так что увеличиваем их количество
        totalResults++;

В этом случае, переменная с URL, который мы передаём, содержит поисковый запрос для термина «data mining».

Как видите, сначала мы делаем запрос, чтобы получить содержимое страницы. Затем загружаем полученные данные в Cheerio, чтобы иметь возможность получать из DOМ элементы, содержащие релевантные ссылки. Затем перебираем ссылки и обрезаем ненужные нам параметры URL, которые добавляет Google для своих целей (нам они не нужны).

Наконец, когда мы всё это сделаем, нужно убедиться что URL не начинается с /. Если начинается — это внутренняя ссылка на ресурс Google, и нам её содержимое скачивать не нужно.

Получаем слова со страниц

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

request(url, function (error, response, body) {
  // загружаем страницу в Cheerio
    var $page = cheerio.load(body),
        text = $page("body").text();

Снова используем Request и Cheerio, чтобы скачать страницу и получить доступ к её DOM. В данном примере мы получаем со страницы только текст.

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

  1. Сжать все пробелы в одиночные пробелы.
  2. Выбросить все символы, которые не являются буквами или пробелами.
  3. Перевести всё в нижний регистр.

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

Код, который всё это делает, выглядит приблизительно так:

// избавляемся от лишних пробелов и нечисловых символов.
text = text.replace(/\s+/g, " ")
         .replace(/[^a-zA-Z ]/g, "")
         .toLowerCase();

// разбиваем по пробелу, чтобы получить список слов на странице,
// и перебираем их в цикле.
text.split(" ").forEach(function (word) {
  // скорее всего, нам не нужно включать слишком короткие или слишком длинные слова,
  // так как они, скорее всего, содержат бесполезные для нас данные.
    if (word.length  20) {
        return;
    }

    if (corpus[word]) {
    // если слово уже находится в словаре, нашей коллекции
    // терминов, увеличиваем количество его вхождений на единицу.
        corpus[word]++;
    } else {
    // В противном случае, считаем, что встречаем его впервые.
        corpus[word] = 1;
    }
});

Анализируем слова

Когда все слова добавлены в словарь, мы можем перебрать их в цикле и отсортировать по популярности. Сперва надо внести их в массив, так как словарь — объект.

// поместим все слова в словарь
for (prop in corpus) {
    words.push({
        word: prop,
        count: corpus[prop]
    });
}

// сортируем массив основываясь на частоте вхождения слов
words.sort(function (a, b) {
    return b.count - a.count;
});

Результатом будет отсортированный массив, показывающий как часто каждое слово используется на сайтах с первой страницы поисковой выдачи Google. Ниже простой список результатов для термина «data mining» (по некому совпадению я использовал этот список, чтобы сгенерировать облако тегов вначале статьи).

[ { word: 'data', count: 981 },
  { word: 'mining', count: 531 },
  { word: 'that', count: 187 },
  { word: 'analysis', count: 120 },
  { word: 'information', count: 113 },
  { word: 'from', count: 102 },
  { word: 'this', count: 97 },
  { word: 'with', count: 92 },
  { word: 'software', count: 81 },
  { word: 'knowledge', count: 79 },
  { word: 'used', count: 78 },
  { word: 'patterns', count: 72 },
  { word: 'learning', count: 70 },
  { word: 'example', count: 70 },
  { word: 'which', count: 69 },
  { word: 'more', count: 68 },
  { word: 'discovery', count: 67 },
  { word: 'such', count: 67 },
  { word: 'techniques', count: 66 },
  { word: 'process', count: 59 } ]

Если интересно увидеть остальную часть кода, посмотрите на полностью комментированный исходный код.

Интересным опытом было бы вывести это приложение на новый уровень. Можете оптимизировать анализ текста, расширить поиск на множество страниц поисковой выдачи, удалить слова, которые не являются ключевыми (такие, как «that» и «from»). Улучшить обработку ошибок, чтобы сделать приложение ещё более точным: когда вы занимаетесь дата-майнингом, лучше иметь столько слоёв проверок, сколько возможно. При том многообразии данных, которое можно получить таким образом, неизбежно найдётся блок текста, который, не будучи обработанным должным образом, непременно приведёт к возникновению ошибки и аварийному завершению работы приложения.

Заключение

Как всегда, если вы нашли что-то по теме как «грабить» сайты с помощью Node.js или у вас есть вопросы — дайте знать в комментариях. А ещё можете читать меня в Twitter и периодически заглядывать в мой блог в поисках новых статей о Node.js, «грабеже» и JavaScript в целом.

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Elliot Bonneville
Антон Немцев
Переводчик:
Антон Немцев
Сaйт:
http://frontender.info/
Twitter:
@silentimp
GitHub:
SilentImp
Skype:
ravencry

Комментарии (2 комментария, если быть точным)

Автар пользователя
jt3k

Полезнота и киллерфитчество по сравнению с пшп)

Автар пользователя
EugeneTM

Пример не совсем рабочий, потому что нужно дополнительно парсить вывод $page("body").text(). Сейчас результаты такие:

[ { word: 'function', count: 96 }, { word: 'important', count: 58 }, { word: 'true', count: 54 }, { word: 'idyandexadhorizontal', count: 45 }, { word: 'idyandexad', count: 42 }, { word: 'height', count: 33 }, ...]