Перейти к содержимому

Новый спайдер на основе htmlparser + soupselect

01/02/2011

На досуге набросал новый спайдер для очередного микропроекта. В этот раз я решил не использовать jsdom (как в нескольких предыдущих), а остановиться на связке htmlparser + soupselect. Страница сначала отдаётся парсеру, а поиск по полученному DOM делается с помощью SoupSelect.

Архитектура спайдера

Архитектура самого спайдера тоже отличается от предыдущего. На этот раз в основе лежит EventEmitter, а состояния спайдера реализованы в виде событий. События всего два — ready, означающее что спайдер готов запросить и обработать новую страницу, и new_friend, означающее что получен отдельный результат. Сейчас спайдер парсит страницы пользователей Хабра, и событие new_friend означает что в профиле пользователя найдена ссылка на друга.

Код выглядит так:

var spider = new process.EventEmitter();

var users_to_go = [];

spider.on('ready', function() {
    if (users_to_go.length == 0) {
        console.log('We\'re done');
        process.exit(1);
    }

    // Получаем следующего юзера
    var user = users_to_go.shift();

    // Получаем друзей из его профиля
    get_friends(user.username, function(err, data) {
        if (!err) {
            data.forEach(function(friend) {
                // Обнаружена ссылка на другой профиль
                spider.emit('new_friend', friend, user.level + 1, user.path + ' ' + user.username);
            });
        }

        // Переводим спайдер в состояние ready через 4 секунды
        setTimeout(function() {
            spider.emit('ready');
        }, 4000);
    });
});

spider.on('new_friend', function(username, level, path) {
    // Событие обнаружения новой ссылки. Здесь мы можем делать с ней что угодно.
});

// Начальный пользователь
users_to_go.push({username: 'shoohurt', level: 0, path:''});

// Запускаем спайдер, переводя его в состояние ready
spider.emit('ready');

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

HTMLparser и SoupSelect

Внутри функции get_friends происходит создание HTTP-клиента, получение страницы и её обработка. Вот на обработке я хочу остановиться подробнее.

Сначала полный текст страницы передаётся htmlparser’у, на выходе получается DOM. Сделать это можно двумя способами, в зависимости от того где Вы взяли модуль htmlparser’а (npm или github). Версия с Github работает так:

var html = require('./node-htmlparser');

var dom = html.ParseHtml(text);

Версия из npm отличается способом подключения (из всех названий модулей npm выбрасывают слово node) и способом создания dom:

var html = require('htmlparser');

var handler = new htmlparser.DefaultHandler(function (error, dom) {});
var parser = new htmlparser.Parser(handler);
parser.parseComplete(text);
var dom = handler.dom;

Я сначала пользовался версией из Github, потом починил таки npm и перешёл на версию оттуда. После получения DOM мы можем выполнять по нему поиск с помощью soupselect:

var select = require('soupselect');

var friends = [];
select(dom,'dl.friends_list a').forEach(function(friend){ friends.push(friend.attribs.href.match(/\/\/([^.]*).habrahabr.ru/)[1])});

Модулю передаётся dom и селектор, на выходе получается массив найденных элементов. DOM в htmlparser немного не такой как в браузере, но структура довольно понятна (больше информации можно найти в документации htmlparser). Связка htmlparser+soupselect работает быстрее чем jsdom+jquery.

Кроме того, в спайдере используется коннектор для Redis (npm install redis), с очень удобным API и стабильной работой. Помещение награбленного контента в Redis происходит в обработчике события new_friend. Вообще, для более открытой архитектуры стоит ещё разделить функцию get_friends на отдельные события обработки страницы — получение кода статуса и заголовков, получение текста ответа, построение DOM. Тогда можно будет подключать готовый спайдер целиком и просто вешать обработчики на готовые события. Также в коде спайдера нужен пул соединений и поддержка вытягивания страниц в несколько потоков, но это всё будет потом.

Ссылки по теме

4 комментария
  1. Надо будет, наконец, написать что-то серьёзное на Ноде 🙂

    Имхо, не стоит использовать master с гитхаба как раз из-за постоянного изменения API. Я ещё понимаю, когда пишут багрепорты на эту тему контрибьюторы проекта, но ведь часто пишут пользователи, которым не всегда понятно, что master в Git обычно содержит нестабильную версию и нужно использовать последнюю тегированную версию.

    P.S. Нужно при экспорте на nodejs.ru относительные ссылки править, http://nodejs.ru/2010/04/15/node-js-pseudo-threads/ ведёт на 404.

    • Опс 🙂 Ссылки должны становиться абсолютными сами (через XSLT), похоже что то сломалось в преобразователе ) Спасибо что указал.

  2. Посмотри на Scrapy http://scrapy.org/
    Оно на питоне, если с питоном не знаком, хотя бы архитектуру можно подглядеть, она там очень удачная. Тоже асинхронное, работает на ура.

Trackbacks & Pingbacks

  1. Новый спайдер на основе htmlparser + soupselect « nodeJS

Оставьте комментарий