Тема веб-скрейпинга вызывает всё больше интереса как минимум потому, что это неисчерпаемый источник небольших, но удобных и интересных заказов для фрилансеров. Естественно, что всё больше людей пытаются выяснить, что это такое. Однако, довольно трудно понять, что такое веб-скрейпинг по абстрактным примерам из документации к очередной библиотеке. Гораздо проще разобраться в этой теме наблюдая за решением реальной задачи шаг за шагом.


Обычно, задача для веб-скрейпинга выглядит так: есть данные, доступные только на веб-страницах, и их надо оттуда вытащить и сохранить в неком удобоваримом формате. Конечный формат не важен, так как конвертеры никто не отменял. По большей части речь о том, чтобы открыть браузер, пройтись мышкой по ссылкам и скопипейстить со страниц нужные данные. Ну, или сделать то же самое скриптом.


Цель этой статьи – показать весь процесс создания и использования такого скрипта от постановки задачи и до получения конечного результата. В качестве примера я рассмотрю реальную задачу вроде тех, какие часто можно найти, например, на биржах фриланса, ну, а в качестве инструмента для веб-скрейпинга будем использовать Node.js.


Постановка задачи


Допустим, я хочу получить список всех статей и заметок, которые я опубликовал на сайте Ferra.ru. Для каждой публикации я хочу получить заголовок, ссылку, дату и размер текста. Удобного API у этого сайта нет, так что придётся скрейпить данные со страниц.


Отдельный раздел на сайте я за долгие годы так и не удосужился организовать, так что все мои публикации лежат вперемешку с обычными новостями. Единственный известный мне способ выделить нужные мне публикации – фильтрация по автору. На страницах со списком новостей автор не указан, так что придётся проверять каждую новость на соответствующей странице. Я помню, что писал только в раздел “Наука и технологии”, так что искать можно не по всем новостям, а по одному разделу.


Вот, примерно в таком виде мне обычно поступают задачи для веб-скрейпинга. Ещё в таких задачах бывают разные сюрпризы и подводные камни, но сходу их не видно и приходится их обнаруживать и улаживать прямо в процессе. Начнём:


Анализ сайта


Нам будут нужны страницы с новостями, ссылки на которые собраны в паджинированном списке. Все нужные страницы доступны без авторизации. Посмотрев в браузере исходники страниц можно убедиться, что все данные содержатся прямо в HTML-коде. Довольно простая задача (собственно, я потому её и выбрал). Похоже, нам не придётся возиться с логином, хранением сессии, отправкой форм, отслеживанием AJAX-запросов, разбором подключённых скриптов и так далее. Бывают случаи, когда анализ целевого сайта занимает в разы больше времени, чем проектирование и написание скрипта, но не в этот раз. Может быть в следующих статьях...


Подготовка проекта

Подготовка проекта


Думаю, нет смысла описывать создание каталога проекта (а в нём пустого файла index.js и простейшего файла package.json), установку Node.js и пакетного менеджера npm, а также установку и удаление модулей через npm.


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


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


Чтобы получить данные из HTML-кода страницы надо получить этот код с сайта. Это можно делать при помощи http-клиента из модуля http, встроенного в Node.js по умолчанию, однако для выполнения простых http-запросов удобнее использовать разные модули-обёртки над http самым популярным из которых является request так что попробуем его.


Первым делом стоит убедиться, что модуль request получит с сайта такой же HTML-код, какой приходит в браузер. С большинством сайтов это так и будет, но иногда попадаются сайты, отдающие браузеру одно, а скрипту с http-клиентом – другое. Раньше я первым делом проверял целевые страницы GET-запросом из curl, но однажды мне попался сайт, который в curl и в скрипт с request выдавал разные http-ответы, так что теперь я сразу пробую запускать скрипт. Примерно вот с таким кодом:


var request = require('request');

var URL = 'http://www.ferra.ru/ru/techlife/news/';

request(URL, function (err, res, body) {
    if (err) throw err;
    console.log(body);
    console.log(res.statusCode);
});

Запускаем скрипт. Если сайт лежит или с подключением проблемы, то вывалится ошибка, а если всё хорошо, то прямо в окно терминала вывалится длинная простыня исходного текста страницы, и можно убедиться, что он практически такой же, как и в браузере. Это хорошо, значит нам не понадобится устанавливать специальные куки или http-заголовки чтобы получить страницу.


Однако, если не полениться и промотать текст вверх до русскоязычного текста, то можно заметить, что request неправильно определяет кодировку. Русскоязычные заголовки новостей, например, выглядят вот так:


4,7-???????? iPhone 7 ?????????? ?? ????
PC-?????? DOOM ??????? ?????? 4 ???????? ???????????

Проблема с кодировками сейчас встречается не так часто, как на заре интернета, но всё же достаточно часто (а на сайтах без API – особенно часто). В модуле request предусмотрен параметр encoding, но он поддерживает только кодировки принятые в Node.js для преобразования буфера в строку. Напомню, это ascii, utf8, utf16le (она же ucs2), base64, binary и hex, тогда как нам нужна windows-1251.


Самое распространённое решение для этой проблемы – в request устанавливать encoding в null, чтобы он помещал в body исходный буфер, а для его конвертации использовать модуль iconv или iconv-lite. Например вот так:


var request = require('request');
var iconv  = require('iconv-lite');

var opt = {
    url: 'http://www.ferra.ru/ru/techlife/news/',
    encoding: null
}

request(opt, function (err, res, body) {
    if (err) throw err;
    console.log(iconv.decode(body, 'win1251'));
    console.log(res.statusCode);
});

Минус этого решения в том, что на каждом проблемном сайте придётся тратить время на выяснение кодировки. Если этот сайт – не последний, то стоит найти более автоматизированное решение. Если кодировку понимает браузер, то её должен понимать и наш скрипт. Путь для настоящих гиков – найти на GitHub модуль request и помочь его разработчикам внедрить поддержку кодировок из iconv. Ну, или сделать свой форк с блэкджеком и хорошей поддержкой кодировок. Путь для опытных практиков – поискать альтернативу модулю request.


Я в подобной ситуации нашёл модуль needle, и остался настолько доволен, что больше request не использую. С настройками по умолчанию needle определяет кодировку точно также, как это делает браузер, и автоматически перекодирует текст http-ответа. И это не единственное, в чём needle лучше, чем request.


Попробуем получить нашу проблемную страницу при помощи needle:


var needle = require('needle');

var URL = 'http://www.ferra.ru/ru/techlife/news/';

needle.get(URL, function(err, res){
    if (err) throw err;
    console.log(res.body);
    console.log(res.statusCode);
});

Теперь всё замечательно. Для очистки совести стоит попробовать то же самое со страницей отдельной новости. Там тоже всё будет хорошо.


Краулинг


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


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


Для создания такой очереди можно использовать функцию queue из знаменитого модуля async, однако я предпочитаю использовать модуль tress, который обратно совместим с async.queue, но намного меньше, так как не содержит остальных функций модуля async. Маленький модуль хорош не тем, что занимает меньше места (это ерунда), а тем, что его проще быстренько допилить, если это понадобится для особо сложного краулинга.


Очередь из tress работает примерно так:


var tress = require('tress');
var needle = require('needle');

var URL = 'http://www.ferra.ru/ru/techlife/news/';
var results = [];

// `tress` последовательно вызывает наш обработчик для каждой ссылки в очереди
var q = tress(function(url, callback){

    //тут мы обрабатываем страницу с адресом url
    needle.get(url, function(err, res){
        if (err) throw err;

        // здесь делаем парсинг страницы из res.body
            // делаем results.push для данных о новости
            // делаем q.push для ссылок на обработку

        callback(); //вызываем callback в конце
    });
});

// эта функция выполнится, когда в очереди закончатся ссылки
q.drain = function(){
    require('fs').writeFileSync('./data.json', JSON.stringify(results, null, 4));
}

// добавляем в очередь ссылку на первую страницу списка
q.push(URL);

Стоит отметить, что наша функция каждый раз будет выполнять http-запрос, и пока он выполняется скрипт будет простаивать. Так скрипт будет работать довольно долго. Чтобы его ускорить можно передать tress вторым параметром количество ссылок, которые можно обрабатывать параллельно. При этом скрипт продолжит работать в одном процессе и в одном потоке, а параллельность будет обеспечиваться неблокирующих операций ввода/вывода в Node.js.


Парсинг


Тот код, который у нас уже есть, можно использовать как основу для скрейпинга. Фактически, мы создали простейший минифреймворк, который можно понемногу дорабатывать каждый раз, как нам попадётся очередной сложный сайт, а для простых сайтов (которых большинство) можно просто писать фрагмент кода, отвечающий за парсинг. Смысл этого фрагмента будет всегда один и тот же: на входе – тело http-ответа, а на выходе – пополнение массива результатов и очереди ссылок. Инструменты для парсинга на остальной код повлиять не должны.


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


Однако, большая часть HTML-страниц легко разбирается DOM-парсерами, которые намного удобнее и легче читаются. Регулярки стоит использовать только если DOM-парсеры не справляются. В нашем случае DOM-парсер отлично подойдёт. В настоящий момент среди DOM-парсеров под Node.js уверенно лидирует cheerio – серверная версия культового JQuery.


(Кстати, на Ferra.ru используется JQuery. Это довольно надёжный признак того, что cheerio с таким сайтом спарвится)


Поначалу может показаться, что удобнее написать отдельный парсер для каждого типа страниц (в нашем случае их два – списки и новости). На деле можно просто искать на странице каждую разновидность данных. Если нужных данных на странице нет, то они просто не найдутся. Иногда приходится подумать, как избежать путаницы если разные данные одинаково выглядят на страницах разного типа, но я ни разу не встречал сайта, где это было бы сложно. Зато я встречал много сайтов, где разные типы данных произвольно сочетались на одних и тех же страницах, так что стоит сразу привыкать писать единый парсер для всех страниц.


Итак, списки ссылок на новости у нас располагаются внутри элемента div с классом b_rewiev. Там есть и другие ссылки, которые нам не нужны, но правильные ссылки легко отличить, так как только у таких ссылок родитель – элемент p. Ссылка на следующую страницу паджинации у нас располагается внутри элемента span с классом bpr_next, и она там одна. На страницах новостей и на последней странице списка такого элемента нет. Стоит учесть, что ссылки в паджинаторе – относительные, так что их надо не забыть привести к абсолютным. Имя автора спрятано в глубине элемента div с классом b_infopost. На страницах списка такого элемента нет, так что если автор совпадает – можно тупо собирать данные новости.


Не стоит забывать и о битых ссылках (спойлер: в разделе, который мы скрейпим, таких ссылок целая одна). Как вариант, можно у каждого запроса проверять код ответа, но бывают сайты, которые отдают страницу битой ссылки с кодом 200 (даже если пишут на ней “404”). Другой вариант – посмотреть в коде такой страницы те элементы, которые мы собираемся искать парсером. В нашем случае таких элементов на странице битой ссылки нет, так что парсер такие страницы просто проигнорирует.


Добавим в наш код парсинг при помощи cheerio:


var tress = require('tress');
var needle = require('needle');
var cheerio = require('cheerio');
var resolve = require('url').resolve;
var fs = require('fs');

var URL = 'http://www.ferra.ru/ru/techlife/news/';
var results = [];

var q = tress(function(url, callback){
    needle.get(url, function(err, res){
        if (err) throw err;

        // парсим DOM
        var $ = cheerio.load(res.body);

        //информация о новости
        if($('.b_infopost').contents().eq(2).text().trim().slice(0, -1) === 'Алексей Козлов'){
            results.push({
                title: $('h1').text(),
                date: $('.b_infopost>.date').text(),
                href: url,
                size: $('.newsbody').text().length
            });
        }

        //список новостей
        $('.b_rewiev p>a').each(function() {
            q.push($(this).attr('href'));
        });

        //паджинатор
        $('.bpr_next>a').each(function() {
            // не забываем привести относительный адрес ссылки к абсолютному
            q.push(resolve(URL, $(this).attr('href')));
        });

        callback();
    });
}, 10); // запускаем 10 параллельных потоков

q.drain = function(){
    fs.writeFileSync('./data.json', JSON.stringify(results, null, 4));
}

q.push(URL);

В принципе, мы получили скрипт для веб-скрейпинга, который решает нашу задачу (для желающих код на gist). Однако отдавать заказчику я бы такой скрипт не стал. Даже с параллельными запросами этот скрипт выполняется долго, а значит ему как минимум нужно добавить индикацию процесса выполнения. Также сейчас, даже при недолгом перебое со связью, скрипт упадёт не сохранив промежуточных результатов, так что надо сделать либо чтобы скрипт сохранял промежуточные результаты перед падением, либо чтобы он не падал, а вставал на паузу. Ещё я бы от себя добавил возможность принудительно прервать работу скрипта, а потом продолжить с того же места. Это излишество, по большому счёту, но такие “вишенки на торте” очень укрепляют отношения с заказчиками.


Однако если заказчик попросил один раз заскрейпить ему данные и просто прислать файл с результатами, то ничего этого можно и не делать. Всё и так работает (23 минуты в 10 потоков, найдено 1005 публикаций и одна битая ссылка). Если совсем обнаглеть, то можно было бы не делать рекурсивный проход по паджинатору, а сгенерировать по шаблону ссылки на страницы списка за тот период, когда я работал на Ferra.ru. Тогда и скрипт не так долго работал бы. Поначалу это раздражает, но выбор вот таких решений – это тоже важная часть задачи по веб-скрейпингу.


Заключение


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


В ближайшей перспективе я планирую статьи про более сложные случаи (сессии, AJAX, глюки на сайте и так далее) и про доведение веб-скрейперных скриптов до товарного вида. Вопросы и пожелания приветствуются.

Поделиться с друзьями
-->

Комментарии (36)


  1. cyber_ua
    24.05.2016 09:37

    Не много оффтоп:
    Нам препод рассказывал что можно парсить страницы с помощью недетерменированых конечных автоматов, как просто парсить например xml/html и строить его дерево например с их помощью я знаю, а вот как проанализировать например веб-страницу на наличие нужно информации?


    1. astur
      24.05.2016 20:57
      +1

      Ну, про НКА стоит думать на этапе написания чего-то вроде cheerio или, например, lxml, а на этапе скрейпинга просто используется готовый парсер по принципу чёрного ящика.

      Анализ наличия нужной информации — это довольно абстрактная тема, так как признаки нужности могут быть очень разными. Что-то можно проверить простенькой регуляркой, а что-то нужно прогонять через мощный синтаксический анализатор на НКА.


  1. vmb
    24.05.2016 12:54

    Спасибо за статью. Недавно я тоже написал две на такую же тему, с чуть более узким предметом:


    https://habrahabr.ru/post/274475/
    https://habrahabr.ru/post/277513/


    Там разбираются многие темы, о которых вы упомянули.


    Сделаю несколько заметок.


    1. Использование JSDOM (в консольных скриптах) или NW.js (в GUI) помогает отказаться от сразу нескольких модулей, поскольку берёт на себя и HTTP, и парсинг DOM, и разрешение ссылок, и правильную обработку кодировок.


    2. В качестве предосторожности от сбоев можно вести два лога: лог данных и лог успешно сохранённых адресов. Если чистовой файл результата нужен в виде единого объекта JSON, в конце работы лог данных можно переконвертировать, если достаточно простого несортированного результата — лог данных и будет таким результатом. Лог адресов можно загружать после прерывания, чтобы, во-первых, проверять по нему ссылки не грузить их по второму разу, и чтобы можно было начинать с последнего успешного адреса.


    3. Если данные нужно сохранять в последовательном порядке (как в случае со словарями), параллельность выполнения будет мешать, если данных нужно сохранить очень много. В моей первой статье я описываю сохранение словаря размером в 2 гигабайта — строить такую структуру в памяти и потом её сортировать очень рискованно. Писать её в файл и потом его сортировать тоже может быть небезопасно (не у всех машины позволят такое провернуть, да и Node нужно будет запускать с ключами увеличения памяти).


    4. Также при асинхронной параллельности нужно учитывать политику сайта для ботов. Иногда её можно подсмотреть в robots.txt, а иногда приходится выяснять экспериментально. Многие сайты забанят особо ретивый скрипт после первых десятков запросов. Тогда приходится даже вставлять искусственные таймауты в однопоточную работу.


    1. astur
      24.05.2016 21:23
      +1

      Статьи хорошие, да )

      1. Никто не мешает собрать несколько модулей в один и использовать его и для всей последовательности задач. Проблема в том, что универсальные модули часто не учитывают много частных случаев, так что постоянно приходится лезть под капот, поэтому стоит иметь представление о том, как там всё устроено. Я и сам использую для скрейпинга собственный модуль, который «берёт на себя и HTTP, и парсинг DOM, и разрешение ссылок, и правильную обработку кодировок» и много чего ещё делает, но часто попадаются сайты, которые он не в состоянии обработать. Приходится к дедлайну всё делать на коленке из специализированных модулей, а потом, в спокойной остановке, допиливать универсальный модуль на будущее. Так он и развивается.

      2. Всё верно. Вообще, это отдельная тема, и по ней я тоже планирую отписаться. Просто для начала взял пример попроще, где и без этого можно справиться, чтобы статья в здоровенный учебник не превращалась :)

      3. Упорядочить можно уже готовые данные. Надо просто предусмотреть соответствующие поля заранее. Огромные структуры скрейпить в память — сизифов труд, да. В идеальном мире стоит писать в БД и проверять ошибки при перезапусках. Но в реальном мире заказчики иногда просят сохранение в читабельные файлы (например, им лень или трудно поднять БД на своей стороне), так что приходится выкручиваться, и не только с сортировкой. Про скрейпинг больших объёмов тоже будет статья, да.

      4. Всякое бывает. Некоторые сайты честно просят не пинать их ботами чаще, чем раз в Х секунд, например. Теоретически, можно скрейпить параллельно через несколько прокси, и блокировки не будет, но можно, например, положить сервер нагрузкой, что как минимум не этично. Надо смотреть по ситуации. Конкретно в случае с Ferra.ru я запрашивал у главреда разрешение на использование скрейпера.

      Короче, по каждому из четырёх пунктов стоит ответить полноценной статьёй :)


  1. Flammar
    24.05.2016 13:16
    +1

    Делал подобное в 2001 на Java через HTTPS (JSSE) с паролями, сессией и cookies и «страницей подробностей» на каждую строчку…

    Не знал, что это называется «Web scraping»…


    1. astur
      24.05.2016 21:27

      Ух ты! В 2001 году это был тот ещё экстрим.

      С терминологией в этой области до сих пор бардак. Возможно решусь сделать словарик отдельной статьёй. Пока в поисковике приходится десятки терминов пробивать, чтобы ничего не упустить. Термин «Web scraping» более-менее устоялся на англоязычных биржах фриланса.


  1. kirill3333
    24.05.2016 13:59
    +3

    Многие сайты борются с этим. Из своего опыта, хорошие инструменты PhantomJS и SlimerJS (и обертка над ними в виде CasperJS). Но и эти инструменты не идеальны, так как существуют достаточно простые методы для детектирования headles браузеров. После этого остается только использовать настоящие браузеры например при помощи WebdriverIO


    1. vmb
      24.05.2016 14:02

      Есть ещё проекты NW.js и Electron, связывающие Node.js и Chromium.


      1. kirill3333
        24.05.2016 14:06

        Мне кажется у них немного иное предназначение, хотя могу ошибаться, использовал только Atom построенный на базе Electron


        1. vmb
          24.05.2016 14:18

          Да, у них очень широкое предназначение, но в том числе и такое. Я использовал NW.js для копирования сетевого словаря, довольно удобно и надёжно.


          1. astur
            24.05.2016 21:38
            +1

            Как минимум, NW хорош, когда заказчик просит не гиковский скрипт для возни с шеллом, а обычную программу для простых людей.


    1. Flammar
      24.05.2016 17:09
      +1

      До сих пор из такой борьбы видел только фильтрацию по юзер-агенту, что в Java обходится программным задаванием некоторых заголовочных полей (этого самого юзер-агента) в свойствах не-помню-уже-чего.

      А какие методы детектирования headless-браузеров имеете в виду вы? анализ нагрузки, генерируемой с одинаковыми cookies с одинакового IP?


      1. kirill3333
        25.05.2016 11:06

        Вот попробуйте попарсить это www.hemmings.com


      1. kirill3333
        25.05.2016 11:09
        +2

        Про методы я скинул ссылку на слайды выше и вот например статья там все намного хитрее — например специально вызывается JS ошибка и смотрится стэктрейс


    1. astur
      24.05.2016 21:36
      +1

      Да, бывают сайты, где только Selenium, только хардкор. К счастью, их мало :)

      Борются часто не со скрейпингом, а, например, с ботами-коментаторами, с накруткой, с DDoSами, с попытками взлома и так далее, а скрейпинг попадает под раздачу. Но иногда такая хитрая защита означает настойчивую просьбу не скрейпить, и тогда это уже вопрос морали и закона. Но технически через Selenium скрейпится всё, что можно скрейпить вручную.


      1. vmb
        24.05.2016 21:44

        Я недавно натолкнулся на подобную особенность. Сначала долго ломал голову себе и докучал создателям jsdom, почему модуль не видит ссылку на следующую страницу. Весь код и структура идентичны в браузере и скрипте, за исключением ссылки на следующую страницу. Потом меня осенило, я переопределил userAgent в скрипте, и ссылка появилась.


        1. astur
          24.05.2016 22:09

          Знакомо :)


  1. peter23
    24.05.2016 15:29
    +1

    значит нам не понадобится устанавливать специальные куки или http-заголовки чтобы получить страницу.
    Однако, стоит в любом случае передавать заголовки как в браузере. Это поможет защититься от подводных камней.

    используется JQuery. Это довольно надёжный признак того, что cheerio с таким сайтом спарвится
    Полагаю взаимосвязи тут нет и быть не может, принципы работы jQuery и cheerio абсолютно разные, несмотря на схожий интерфейс использования и язык реализации.


    1. astur
      24.05.2016 21:44
      +1

      — needle отправляет некий минимальный набор заголовков. Без этого никак, да.

      — Я имел в виду, что если html достаточно «семантический» для того, чтобы использовать CSS-селекторы в jQuery, то эти же селекторы можно использовать и в cheerio. И то и другое — DOM-парсер в своей основе, так что если на странице используют jQuery, то она под него оптимизирована и cheerio на ней будет хорошо работать. Просто иногда бывает жуткая мешанина из div вообще без атрибутов и сориентироваться можно только по текстовым данным — тогда любые DOM-парсеры справляются плохо (а иногда и регулярки подводят).


  1. gordeevov
    24.05.2016 21:39

    Есть же готовый инструмент, screen-scraper.com
    Бесплатная версия для основных задач такого рода вполне подходит.


    1. astur
      24.05.2016 22:01

      Есть довольно много подобных готовых инструментов. Проблема в том, что они подходят только основные задачи.

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

      Я и сам использую универсальный модуль собственного сочинения и написание скрейпера для простого сайта у меня занимает от получаса, но бывают случаи, когда приходится вспоминать знания низкоуровневых основ. Это происходит всё реже, так как модуль непрерывно совершенствуется после таких случаев, но это всегда будет происходить, так как веб — это очень терпимая к нарушениям и ошибкам среда, где универсальные решения крайне сложно делать. Это я ещё не говорю о том, что даже начинающий хакер легко напишет защиту от сервисов уровня screen-scraper.com.


      1. gordeevov
        24.05.2016 22:38

        Я же не утверждал, что это панацея :)
        Всего лишь инструмент.
        Без понимания именно тех низкоуровневых основ — в любом случае не получится сделать что-то сложное.

        Но инструмент достаточно гибкий и удобный. Сильно помогает в парсинге полученного html, облегчает и ускоряет рутинные задачи, тот же самый анализ заголовков запросов/ответов.

        Защиту от него написать — ну тут не знаю… Он же шлет себе http-запросы… Я же их сам и редактирую, если надо. Как сайт определит, что я не из браузера захожу?

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


        1. astur
          25.05.2016 00:23
          +2

          Я пока не готов написать обзор инструментов для скрейпинга, но среди них много неплохих решений для рутинных задач. Проблема в том, что существуют не только рутинные задачи. Более того, существуют задачи, про которые далеко не сразу можно понять рутинные они или нет.

          Защита пишется влёгкую. Конечно, это типичная «битва брони и снаряда», и пока одни люди совершенствуют защиты — другие люди совершенствуют методы их обхода. Анализ заголовков браузера — это в последние годы и защитой-то не считается. Сейчас сайт определит, что вы зашли не из браузера, так как в ваших http-запросах не будет тех хитрых зашифрованных данных, которые в браузере вычислились бы подключённым js-скриптом, крайне трудным для реверс-инжиниринга. В запрос можно подставить всё — куки, заголовки, данные форм и так далее — однако все эти данные откуда-то нужно получить, а они не обязательно хранятся в http-ответах в открытом виде. Есть и другие способы защиты, вплоть до анализа частоты запросов. Это серьёзно. Чтобы понять, насколько это серьёзно, надо осознать, что большая часть защит пишется не от скрейперов, а от профессиональных взломщиков банковских онлайн-клиентов, а уж потом используется и против скрейперов тоже.


          1. vmb
            25.05.2016 00:35

            Кстати, насчёт защиты вычислением. Хотя этикет и требует, чтобы основные данные на страницах были доступны и при отключении JavaScript, часто бывает, что данные почти целиком генерируются скриптами. В таком случае мало скачать код и распарсить его регулярками или dom-парсером. Нужен инструмент, который сможет выполнить скрипты в правильном контексте с поддержкой всех нужных стандартов, и построить дерево динамически. Тут как раз и понадобятся средства вроде jsdom или интерфейсов к браузерным движкам.


            1. ckr
              25.05.2016 00:49
              +2

              Такие инструменты есть:


              1. astur
                25.05.2016 07:52
                +1

                Да, их много. Для Node.js ещё вот эти есть:



            1. astur
              25.05.2016 01:06

              В большинстве таких случаев данные не генерятся, а запрашиваются по ajax или как-то ещё, но в открытом виде. Или генерятся каким-то очевидным образом. Если данные трудно получить без таких вещей как PhantomJS, то владелец сайта точно против того, чтобы его скрейпили. И стоит задуматься, что у него могут быть не только хорошие хакеры, но и хорошие юристы. В любом случае, весь код страницы открыт для реверс-инжиниринга, вопрос только в трудоёмкости.


            1. gearbox
              25.05.2016 10:06
              +1

              Для тяжелых случаев вполне подойдет selenium, странно что его не упомянули.


          1. gordeevov
            25.05.2016 00:50
            +1

            Знаю это.
            Я почему и пишу, пару лет назад как раз и занимался этим. Сейчас — нет.
            И тогда как раз скрин-скрейпер мне реально помог. Правда там был enterprise edition, у заказчиков был куплен.
            С защитой постоянно сталкивался.
            Всё обходилось легко, всякие сессии, cookies и т.д. — влёт, это вообще не вопрос. Эмуляция запроса-ответа, потом следующий запрос с новыми данными. А js я же в ответе получаю, его же и использую.
            AJAX — вызывал проблемы, там уже руками приходилось писать, но тоже решаемо.

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

            С другой стороны — стандартные задачи они стандартно и решаются… У меня обычно было что-то типа каталога на сайте. Сначала поиск по одному критерию, получаешь несколько страниц списков. По всем страницам пройти, зайти в каждый элемент списка и оттуда взять данные, сохранить в удобоваримой форме.
            Доходило до 4-5 дней на один сайт (с паузами, чтобы не блокировали по частым запросам)
            Но написание скрипта на каждый сайт (а их больше 500 было) в итоге занимало по полчаса.


  1. ckr
    24.05.2016 23:11
    +1

    Ох, задача древняя как IE8. Видел статью на русском еще с nodejs-v0.4.0. Сейчас даже не могу найти.

    Хоть я и фанат nodejs, такие несложные задачи обычно решаю на bash с использованием curl и pup. Если не заморачиваться с кодированием выходной информации, скрипт получается гораздо короче. Очень удобно использовать, например, для проверки баланса на счете или для определения инфы по внешнему ip…


    1. astur
      25.05.2016 00:09
      +1

      Я бы даже сказал, что задача древняя как Netscape. Но веб развивается, появляются сложности, которых раньше не было. Я занимался скрейпингом ещё до IE8 и берусь утверждать, что скрейпинг «уже не торт».

      Тема выбора языка для скрейпинга тянет на отдельную статью. Для проверки баланса и других подобных задач bash более чем достаточно. Тут я согласен. Но если нужно пройтись по десятку другому тысяч ссылок — тут уже стоит использовать… ну, хотя бы perl :)

      Смысл статьи в том, чтобы на простом примере показать те основы, которые нужно хорошо знать, чтобы поверх них проектировать решения сложных задач. Естественно я планирую развивать тему.


      1. ckr
        25.05.2016 00:43

        Я бы даже сказал, что задача древняя как Netscape.

        Я имел в виду задача для nodejs древняя.

        По поводу выбора языка, на данный момент bash по гибкости не сильно уступает perl. Многие используемые bash`ом инструменты по обработке текстов написаны на perl. А уж проверить баланс или спарсить внешний ip откуда-нибудь — задача вообще одной строчки.


        1. astur
          25.05.2016 01:09

          Ну… скрипт на perl или несколько скриптов на perl соединённые через bash — тут слишком тонкая грань. Но по сути я с вами согласен, парсинг нескольких незащищённых страниц — это пустячная задача.


  1. Arceny
    25.05.2016 13:28

    grab.spider на python куда удобнее :-)
    а тут получается спагетти из кода. Хотя node.js мне нравится, но для парсинга/скрапинга использую python.


    1. astur
      25.05.2016 13:46

      Да, хорошая штука. Для node.js нечто подобное только ещё предстоит написать.


      Суть в том, что подобные фреймворки появляются только тогда, когда реальный практик берётся изучать и использовать самые основы, а не выбирает из имеющихся недорешений и костылей к ним. Я на 100% уверен, что lorien умеет скрейпить без фреймворка, просто для него это пройденный этап.


      1. Arceny
        25.05.2016 13:47

        Собственно как я понимаю, этот фреймворк родился в процессе скрейпинга «с нуля» и его архитектура мне очень нравится, за исключением пары моментов, таких как плохая работа через socks прокси, но это больше проблема pycurl (multicurl)