Pull to refresh

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

Reading time3 min
Views63K
Раньше основной библиотекой для парсинга был 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); // цепляем в цепь новые задачи на парсинг, как старые выполнятся.
        });
    }
});


UPDATE:

Проблемы с кодировками при работе с Node.JS:

Чтобы работать с одной из тех кодировок, которых нет изначально в Node.JS, нужно принять данные как Buffer — для этого нужно request вызвать с encoding: null и потом, используя библиотеку iconv уже разобрать.

Пример с кодировкой

var request = require('request');
var Iconv = require('iconv').Iconv;
var fromEnc = 'cp1251';
var toEnc = 'utf-8';
var translator = new Iconv(fromEnc,toEnc);
request(
	{
		url:'http://winrus.com/cpage_r.htm',
		encoding:null
	},
	function(err,res,body){
		console.log(translator.convert(body).toString());
	}
);
Tags:
Hubs:
Total votes 18: ↑12 and ↓6+6
Comments13

Articles