В качестве инструмента для парсинга сайта я использую SlimerJS.
Пример постарался привести как можно в более упрощенном и универсальной форме.
Итак, точка входа:
Первая часть этого файла представляет собой общую логику относящуюся к работа SlimerJs
script.js
var grab = require('./grab'); // подключение модуля
grab.create().init('/catalog'); // инициализация парсера
Здесь происходит подключение модуля парсера и его инициализация, а в метод init() передается URL страницы каталога товаров, ссылка является относительной. Основной домен сайта задается в файле config.js
Вся логика парсера находиться в файле grab.js. Я его разделил на две части, первая часть представляет собой объект обертку над SlimerJS для одновременной работы нескольких копий браузера.
Все комментарии по коду я вынес в листинг в целях упрощения понимания кода.
grab.js
var file = require("./file").create(); // подключение модуля для работы с файловой системой
var config = require("./config").getConfig(); // подключение глобальных переменных
/**
* создаем объект-конструктор
*/
function Grab() {
this.page; // храним текущий объект "webpage"
this.current_url; // сохраняем текущий URL
this.parentCategory; // сохраняем категорию продукта
/**
* метод инициализирует объект
* @param url string относительный адрес ( /contacts )
* @param parent
*/
this.init = function(url, parent) {
this.page = require("webpage").create(); // создаем объект webpage
this.callbackInit(); // определяем callback для объекта webpage
if(url) { // если параметра нет, то обращаемся к домену
config.host += url;
}
this.parentCategory = parent;
this.open(config.host); // открыть URL
};
/**
* открыть URL
* @param {string} url адрес который нужно открыть
*/
this.open = function(url) {
/*
* место для возможной бизнес логики
*/
this.page.open(url);
};
/**
* завершить работы с текущим окном
*/
this.close = function() {
this.page.close()
};
/**
* инициализация callback
*/
this.callbackInit = function() {
var self = this;
/**
* метод вызывается при возникновение ошибки
* @param {string} message error
* @param {type} stack
*/
this.page.onError = function (message, stack) {
console.log(message);
};
/**
* метод вызывается при редиректе или открытий новой страницы
* @param {string} url новый URL
*/
this.page.onUrlChanged = function (url) {
self.current_url = url; // сохраняем URL как текущий
};
/**
* метод вызывается, если в объекте webpage срабатывает метод console.log()
* @param {string} message
* @param {type} line
* @param {type} file
*/
this.page.onConsoleMessage = function (message, line, file) {
console.log(message); // выводим его в текущую область видимости
};
/**
* метод вызывается при каждой загрузке страницы
* @param {string} status статус загрузки страницы
*/
this.page.onLoadFinished = function(status) {
if(status !== 'success') {
console.log("Sorry, the page is not loaded");
self.close();
}
self.route(); // вызываем основной метод бизнес логики
};
};
}
Вторая часть файла, определяет поведение и расширяет созданный объект Grab
Grab.prototype.route = function() {
try {
// если текущая страница это страница содержащая категории продуктов
if(this.isCategoryPage()) {
var categories = this.getCategories(); // спарсить данные со страницы категории
file.writeJson(config.result_file, categories, 'a'); // записать данные в файл
for (var i = 0; i < categories.length; i++) { // пройти все категории товаров
var url = categories[i].url_article; // получаем URL на страницу с товаром текущей категории
new Grab().init(url, categories[i].title); // открываем новую страницу
slimer.wait(3000); // ждем 3 секунды, до открытия следующей страницы
}
} else {
// текущая страница является карточкой товара
var content = this.getContent(); // спарсить данные с карточки товара
file.writeJson(config.result_file, content, 'a'); // записать результат в файл
this.close(); // закрыть текущее окно
}
this.close();
} catch(err) {
console.log(err);
this.close();
}
};
/**
* получить со страницы весь контент, относящийся к категориям
* @returns {Object}
*/
Grab.prototype.getCategories = function() {
return this.getContent('categories')
};
/**
* проверить, содержит ли текущая страница категории товаров
* @returns {bool}
*/
Grab.prototype.isCategoryPage = function() {
return this.page.evaluate(function() {
// определить, присутствуют ли данные, относящиеся к странице товаров
return !$(".catalog-list .item .price").length;
});
};
/**
* получить полезные данные со страницы
* @param {string} typeContent какие данные нужно получить {categories|product}
* @returns {Object}
*/
Grab.prototype.getContent = function(typeContent) {
var result = this.page.evaluate(function(typeContent) {
var result = [];
// находим блок, в котором находятся структурированные данные (страница категорий и продуктов имеют одинаковую разметку)
$(".catalog-list .item").each(function(key, value) {
var $link = $(value).find('a.name'); // кешируем ссылку
var obj = { // собираем данные относящиеся к категории
'type': 'category',
'title': $link.text().trim().toLowerCase(), // заголовок категории
'url_article': $link.attr('href'), // ссылка на товары входящие в эту категорию
'url_article_image': $(value).find('a.img > img').attr('src')
};
// если это карточка товара, то собираем данные относящиеся к карточке товара
if(typeContent !== 'categories') {
obj.size = [];
obj.type = 'product';
$('.razmers:first .pink').each(function(key, value) { // размеры|цвет|диагональ...
obj.size.push($(value).text().trim());
});
obj.price = parseInt($(value).find('.price').text(), 10); // цена
}
result.push(obj);
});
return result;
}, typeContent);
return result;
};
exports.create = function() {
return new Grab();
};
Для удобной работы с файловой системой в SlimerJS предусмотрен API, который позволяет как читать, так и записывать данные
file.js
var fs = require('fs');
/**
* инициализация объекта обертки
*/
function FileHelper() {
/**
* чтение данных
* @param {string} path_to_file относительный путь до файла
* @returns array - данные
*/
this.read = function(path_to_file) {
if(!fs.isFile(path_to_file)){
throw new Error('File ('+path_to_file+') not found');
}
var content = fs.read(path_to_file);
if(!content.length) {
throw new Error('File ('+path_to_file+') empty');
}
return content.split("\n");
};
/**
* записать данные в файл
* @param {string} path_to_file относительный путь до файла
* @param {string} content данные для записи
* @param {string} mode режимы работы 'r', 'w', 'a/+', 'b'
*/
this.write = function(path_to_file, content, mode) {
fs.write(path_to_file, content, mode);
}
/**
* запись данных в виде JSON
* @param {string} path_to_file относительный путь до файла
* @param {array} content данные для записи
* @param {string} mode режимы работы 'r', 'w', 'a/+', 'b'
*/
this.writeJson = function(path_to_file, content, mode) {
var result = '';
for(var i=0; i < content.length; i++) {
result += JSON.stringify(content[i]) + "\n";
}
this.write(path_to_file, result, mode);
}
}
exports.create = function() {
return new FileHelper();
};
И последний файл, это файл конфигурации, в котором можно указать переменные, общие для всей системы
config.js
var Config = function() {
this.host = 'http://example.ru';
this.log_path = 'logs\\error.txt';
this.result_file = 'result\\result.txt';
};
exports.getConfig = function() {
return new Config();
};
Результат работы будет в виде файла, который можно будет обработать для дальнейшего экспорта данных.
Запускается скрипт командой из консоли
slimerjs script.js
Исходники
Комментарии (38)
Tur1st
05.10.2015 21:34использование того или иного инструмента обусловлена в первую очередь тем какой сайт придется парсить, незнаю каким образом вы парсите динамически подгружаемый контент или ситуации когда во время загрузки сайта появиться каптча.
dmx102
06.10.2015 08:57-2Динамически подгружаемый контент парсится на много легче когда он вынесен в api сайта, потому что, как правило, там обёрточных данных меньше и можно задавать доп параметры, в особенности те, что нужны. Например кол-во элементов в выдаче, или поиск. Еще api менее всего уделяют внимание в плане защиты. Более того, когда есть такой api, почти вся работа ложится именно на него
Tur1st
06.10.2015 09:59+4про какой парсинг вообще можно говорить если вы работаете с API?
dmx102
06.10.2015 12:52Я говорю про парсинг тех источников данных, которые создатели сайта используют доя подгрузки динамического контента. Странно что вы не поняли этого. Можно назвать это api или как угодно, сути не меняет.
Tur1st
06.10.2015 13:31+1Если бы все было так просто. Вам видимо не приходилось парсить сайты которые заботятся о своих данных и разными средствами пытаются их защитить. Для примера: запрос на получение данных может не сработать, если предположить что сайт ставит куки о посещенных страницах или вам требуется ввести каптчу без перегрузки страницы
dmx102
06.10.2015 13:40Я работаю с многими сайтами, более того практически в реальном времени (задержка 5-10 мин). Например самая большая доска объявлений по России или сайт, где все продают свои машины.
Протокол HTTP текстовый, куки передаются так де текстом, магии в работе со страницей без ее перезагрузки нет ;)
Как мне кажется, лучшем решением по работе с текстовым протоколом является то, на котором легче всего обрабатывать строки. По этому предпочитаю Perl за его легкость в работе регулярных выражений.
На данный момент нет ни одной адекватной защиты против автоматического распознавания чего либо.Tur1st
06.10.2015 13:48как вы реализовали проблему с каптчами?
dmx102
06.10.2015 13:58В зависимости от конкретного ресурса. Чаще всего просто когда мне предлагают капчу, меняю ip-адрес, тру куки и меняю user-agent. Многие пользуются antigate но я его не использовал.
Простые капчи парсятся через OCR-программы. Они бесплатные, есть консольные и с возможностью обучения.
alekciy
07.10.2015 15:33Всегда есть сервис, который может решить эту проблему. antigate уже упомянули, но в целом их достаточно много. Опят же для интернет магазина капча в контексте парсинга каталога товаров совершенно не актуальна.
Tur1st
07.10.2015 15:38с этими сервисами я знаком, как раз один из них интегрирован в парсер яндекса, но и вопрос в том что как разгадывать каптчи клиент-серверными приложением? допусти каптча возвращается ни как страница, а возникает в процессе работы, т.е грубо говоря аяксом поверх основного контента
alekciy
07.10.2015 16:01Учитывая, что SlimerJS это headless браузер, то имеем полный DOM с рендерингом страницы. Не вижу, чем принципиальная разница между парсингом характеристик товаров и капчей. Это все парсинг данных которые в браузере видит человек, значит спарсить это можно.
Может конечно в SlimerJS с этим какие-то проблемы… Но я в работе использую PhantomJS (а SlimerJS пытается его догнать по функционалу, только на базе FireFox-а) в режиме selenim hub, т.е. управление по webdriver, с написанием бизнес-логики парсинга на XPath. И там уже не важно что, откуда и как вообще грузиться. Видно на странице в браузере? Значит можно стянуть.dmx102
07.10.2015 16:21Я с вами согласен, но хочу добавить, что все зависит от ситуации, потому что когда надо получать данные с множества сайтов делая по 10000 запросов единовременно, SlimmerJS, как и PhantomJS потребляют слишком много ресурсов. Для одного потока конечно нормально. А на счет изменений в верстке, DOM структура так же легко может измениться. Мне PhantomJS нравится и я его использовал регулярно. Так что могу с уверенностью сказать, что все зависит от задачи, но 95% случаев решается оптимальнее на Perl
alekciy
07.10.2015 17:09Конечно DOM может меняться. Именно поэтому и используется XPath. При правильном написании выражений парсер может продолжать работать даже если часть разметки изменилось. Не говоря уже о том, что какие-то вещи на регулярках пишуется слишком сложно и малочитаемо в том время как XPath получается лаконичным и удобно поддерживаемым.
Tur1st
07.10.2015 16:29)) вы видимо не внимательно прочитали мой ответ, я не говорил что с этим проблемы в SlimerJS. Все что касаеться работы с DOM он справляеться более чем отлично. PhantonJS я тоже использовал, но SlimerJs понравился больше тем что позволяет отображать окно браузера, это очень удобно при автоматизации определенных бизнес процессов.
API SlimerJS очень близко к PhantomJS и у разработчиков огромное желание к версии 1.0.0 полностью скопировать API PhantomJsalekciy
07.10.2015 17:14т.е грубо говоря аяксом поверх основного контента
Тогда в чем суть вопроса-то?Tur1st
07.10.2015 17:21вопрос был в том как эту проблему решает dmx102 с помощью perl, так как насколько я понимаю он использует cURL или что то подобное для парсинга
alekciy
07.10.2015 17:48Ах вот какой долгий контекст!
А в perl это решается долгим кастомом когда ручками разбирается вся логика формирования капчи и подпиливается парсер под вытягивание нужных данных. Headless в этом смысле сильно облегчают жизнь возможностью тупо сделать скрин и уже его отправить на распознавание.
dmx102
07.10.2015 17:31Любая капча начинается с инициализации на целевой странице. Затем отдается картинка. Потом следует запрос, верифицирующий переданное значение капчи и параметра ее инициализирующее. В зависимости от сложности капчи она либо скармливается подпрограмме OCR и получается ее расшифрованное значение, либо отдается антигейту и подобным. Других вариантов нет.
На SlimmerJS вы точно так же получите картинку капчи и придете к тому же выбору: «а что же делать дальше?»
Рекоммендую при парсинге просто не допускать ее появления, укладываясь в «разрешенные» (антиспам) лимиты. Они либо вычисляются имперически, либо читаются на специализированных ресурсах.Tur1st
07.10.2015 17:48+1В SlimereJS все проще, перед какой то бизней логикой анализируется изначально известный html блок в котором появляется каптча. Далее. без перегрузки страницы автоматически делается скриншот каптчи, кодируеться в base64 и в том же окне генерируется запрос а API того же антигейта и через пару секунд уже получен результат.
Все это работает без перегрузки целевой страницы.
при таком подходе получается долго парсить ресурс без всяких лимитов, единственное соблюдая определенный таймаутalekciy
07.10.2015 17:54+1Таймауты решаются пачкой прокси. Это еще одна причина по которой я использую PhantomJS. Его можно при запуске зацепить к заданной прокси. К большому сожалению не хватает возможности динамически их менять. Приходится иметь пачку запущенных фантомов — по одному для каждой прокси. А уже приложение по кругу их обходит отправляя задание на парсинг.
Tur1st
07.10.2015 19:58в SlimerJS прокси тоже можно использовать, если честно искать разницу между этими двумя система не вижу смысла, они практически идентичны
alekciy
07.10.2015 17:51Все верно, точно так же. Вопрос, какими силами. Сделать скрин с окна это тупо вызов одного метода в коде. Без волнений вообще как эта капча там появляется.
Безголовые браузеры сильно теряют на потреблении ресурсов и скорости работы, но сильно помогают на сапорте кода парсера.
alekciy
07.10.2015 15:29Большая доска как ни крути это относительно адекватная разметка и работа сайта. А когда сайт поставщика это детища написанное в начале нулевых, и поддерживаемая ХХ разработчиками кто во что горазд, то чисто работы с текстом может не хватать. Потому что мешанина из JS-CSS-HTML такая, что порой на супорте проще поддерживать парсер с возможностью рендеринга страниц, чем каждый раз подстраивать тестовой парсер под изменения, зачастую кривые.
dmx102
07.10.2015 15:36Вы статью читали?
Речь вообще про ШАБЛОННЫЕ интернет-магазины и СТРУКТУРИРОВАННЫЕ данныеalekciy
07.10.2015 16:05А вы?
Я тоже про структурированный данные. Могу даже пошутить «со слабо структурированными данными». Если не приходилось иметь дело со страницами на которых один и тот же шаблон может в итоге давать немного отличающийся результат, то это не значит, что таких ситуаций нет. И я сейчас даже не беру вариант, когда это делается намеренно с цель «защиты».
dmx102
07.10.2015 15:38Единственное, что при рендеренге хорошо, это отображение данных, которые генерятся на js с целью маскировки через обфусцирование
monolithed
05.10.2015 22:35Я для этих целей написал обертку вокруг x-ray + needle, а данные сохраняю в тарантул через tarantool-driver.
dmx102
В чем преимущество парсера на SlimmerJs от парсера написанного на perl или на NodeJs? Зачем так усложнять?
Tur1st
ответил ниже)
faiwer
В сложных ситуациях. Например:
1. Когда есть какие-нибудь браузерные уловки. Правда это, в 99% случаев, будет уже зловредительство. Вроде рассылки спама, или скачивания информации, которую попытались от скачивания защитить.
2. API сервера меняется, чаще вёрстки. В этом случае можно довериться вёрстке :)