Пишем парсер на NodeJS

в 8:20, , рубрики: javascript, node.js, nodejs, parser, метки: , ,

Раньше основной библиотекой для парсинга был JSDOM, который страдал излишней тяжеловесностью и на самом деле тормозил скорее процесс парсинга. Но время изменились и пришел cheerio. Он делает почти все то же самое, и отбрасывает лишние из процесса, при этом сам реализует какую-то часть jQuery(а именно ту, которая нам нужна для парсинга). И за счет этого позволяет наконец написать не тормозящий парсер, при этом не используя regexp'ы ради увеличения производительности. Он справляется и с xml, только нужно вызвать его с {xmlMode: true}. О том как можно легко парсить на nodeJS под катом.

Технологии

Мы будем использовать Q — для создания Defered и построения асинхронной очереди, request — для добычи контента, и cheerio для уже самого парсинга.

Пример в вакууме №1

request(url, function(err, res, body){
  if(err){console.log(err);}
  else{ 
    $ = cheerio.load(body);
    var cards = [];
    $('.card').each(function(){
      cards.push({
          title:$('.title',this).text(),
          url:$('a',this).attr('href')
      });
    });
  }
}

Таким не хитрым образом можно спарсить страницу.

Но что делать если страницы больше, чем одна? У нас появятся 2 проблемы если мы будем решать в лоб не используя Promise. Первая — уход в stack, вторая — уход в память через дублирование scope. Корень всего зла конечно рекурсивная функция, которая нам не сильно подходит при парсинге, соотвественно нам нужно построить асинхронную очередь без увеличения уровня скопа.

Для этого делим нашу программу на 2 этапа:
Этап 1: взятие страницы с пагинатором и узнавание количества страниц всего.
Этап 2: создание асинхронной очереди, в которую мы цепляем нашу парсящую функцию.

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

for(var i = 0; i<l;i+=){
    chain.then(asyncF.bind({page: i}));
}

внутри асинхронной функции должно быть тогда чтение контекста из this.page.

Другой же способ состоит в том чтобы иметь общий поток данных в глобальном виде, а внутрь асинхронных функций просто передавать число которое будет увеличиваться уже в самой асинхронной функции, как это сделано ниже:

Пример в вакууме №2

//stage 1
request('pager',function(err,res, body){ 

    $ = cheerio.load(body);
    var pager = $('.pager');
    var limitPage = parseInt( pager.eq(pager.length-1).text().trim(), 10);
    //stage 2
    function parsePage(page){
        var defer = Q.defer();
        request('/pager/'+page,function(err,res, body){
            if(page<=limitPage){
                defer.resolve(page+1); //инкрементируем счетчик страниц прямо в асинхронной последовательности передавая его в качестве аргумента следующим вызовам
            } else {
                defere.reject();
            }
            //тут код из первого абстракного примера
        });
        //возвращаем promise чтобы на нем построить последовательность.
        return defer.promise;
    }
    var chain = Q.fcall(function(){
        return parsePage(1);
    });
    for(var i = 2; i<limitPage;i++){
        chain = chain.then(function(page){
            return parsePage(page); // цепляем в цепь новые задачи на парсинг, как старые выполнятся.
        });
    }
});

Автор: KlonD90

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js