Web scraping на Node.js і захист від ботів

Це третя стаття в циклі про створення і використання скриптів для веб-скрейпинга за допомогою Node.js.
першої статті розбиралися базові аспекти веб-скрейпинга, такі як одержання і парсинг сторінок, рекурсивний прохід по посиланнях і організація черги запитів. У другий статті розбиралися аналіз сайту, що працює через Ajax, тонка настройка черги запитів та обробка деяких серверних помилок.
Також у другій статті зачіпалася тема ініціалізації сесій, але там мова йшла про гранично простому випадку, коли достатньо виконати один додатковий запит і зберегти куки.
У цій статті розуміється більш складний випадок – ініціалізація сесій з авторизацією по логіну та паролю і з подоланням досить витонченою захисту від ботів. Як зазвичай, на прикладі реального (і вельми популярною серед скрейперов) завдання.
Важлива приміткаУ більшості випадків захист від ботів на сайті спрямована не проти скрейперов, а проти таких речей, як шахрайство, накрутки або спам в коментарях. Проте це завжди зайвий привід задуматися про легальності та етичності скрейпинга саме цього сайту. У цій статті для прикладу обрано сайт, про який добре відомо, що його власники нормально ставляться до скрейпингу (хоча і вважають за краще, щоб він виконувався через спеціальний API). Прості правила: якщо у сайту є відкритий API, значить його власники ради скрейперам, а якщо сайт великий і ультрапосещаемый, навантаження від скрейпинга в обхід API його особливо не потурбує.
У минулих статтях метою було показати весь процес створення і використання скрипта від постановки задачі і до отримання кінцевого результату. У цій статті більша частина аспектів веб-скрейпинга обходиться стороною, а замість цього показується різноманіття підходів до вирішення однієї, досить вузького завдання. Різні методи і інструменти, їх плюси і мінуси, суб'єктивні оцінки, приклади коду, ось це ось все.
Постановка завдання
На цей раз замовник – який інший веб-скрейпер, якому знадобилася допомога колеги. Він хоче отримати (для свого замовника) дані з відомого сайту Amazon. Деякі з потрібних йому даних віддаються тільки авторизованим користувачам. Зрозуміло, у замовника є аккаунт на Amazon, але проблема в тому, що на цьому сайті реалізований захист від автоматичної авторизації. Замовнику потрібен модуль на Node.js, який цей захист проходить. Природно, мова йде про інструмент для автоматичної авторизації своїм акаунтом під свою відповідальність, а не про злом чужого акаунта, наприклад.
Замовник вже виконав аналіз своєї частини завдання. Він переконався, що потрібні йому дані доступні, якщо в http-запиті встановити один заголовок і дві cookies (або все, якщо лінь відокремлювати саме ці дві з авторизованої сесії). Заголовок завжди один і той же, а куки легко можна отримати в браузері зі сторінки авторизованого користувача Amazon (за допомогою DevTools або аналогічного інструменту). Куки застарівають далеко не відразу, так що отримавши їх один раз можна виконати досить багато авторизованих запитів.
Наприклад, ось такий код повинен виводити адресу електронної пошти, доступний тільки авторизованим користувачам Amazon:
var needle = require('needle');
var testURL = 'http://www.amazon.com/gp/profile/A14ZQ17DIPJ6UB/customer_email';
var cookies = {
'session-id': '111-2222222-3333333', // підставити реальний session-id
'ubid-main': '444-5555555-6666666', // підставити реальний ubid-main
};

work(cookies);

function work(cookies){
var options = {
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
cookies: cookies
};
needle.get(testURL, options, function(err, res){
if (err) throw err;
console.log(res.body.data.email);
});
}

Відповідно замовнику потрібен модуль, який отримує на вхід логін і пароль від аккаунта, а на виході дає потрібні куки. Щось на зразок цього:
module.exports = function(email, password, work){
// magic
work(cookies);
}

Замовник буде використовувати цей модуль у власному скрипті для веб-скрейпинга Amazon. Може бути навіть прикрутить до власного фреймворку для скрейпинга. Нам це не принципово.
Історична довідкаПерш ніж переходити до аналізу інтерфейсу авторизації на сайті Amazon варто відзначити один важливий момент. Справа в тому, що цей сайт працює з грошима. Його постійно атакують зловмисники, і цікавить їх зазвичай аж ніяк не отримання легальних даних в обхід API, як нашого замовника. Не дивно, що захисні механізми цього сайту постійно удосконалюються. Я ще пам'ятаю часи, коли автоматична авторизація на Amazon здійснювалася одним POST-запитом. Це було задовго до появи Node.js (я тоді скрейпил на Perl). Потім там поступово додалися такі речі, як переадресації, приховані поля форм, одноразові адреси тощо. В мережі досі можна знайти приклади авторизації на Amazon (за допомогою, наприклад, PHP Curl), в яких реалізується приблизно наступний алгоритм:
  1. Отримуємо сторінку з одноразової посиланням на сторінку з формою логіна;
  2. Зберігаємо куки (які підходять саме до цієї одноразової ссылке);
  3. Парсим сторінку і отримуємо потрібну посилання;
  4. Робимо запит по цьому посиланню (використовуючи збережені куки) і отримуємо сторінку;
  5. Зберігаємо куки (вони вже інші, так);
  6. Парсим сторінку і отримуємо значення всіх прихованих полів форми логіна;
  7. Додаємо поля електронної пошти та пароля;
  8. Імітуємо POST-запит з форми (використовуючи останні збережені куки);
  9. Отримуємо відповідь з кодом 302 та адресою в заголовку Location;
  10. Зберігаємо куки (третій варіант)
  11. Отримуємо сторінку за адресою Location (знову використовуючи кукі)
  12. Нарешті-то отримуємо ті куки, які нам потрібні, і використовуємо їх для авторизованих запитів.
Досвідчені скрейперы можуть помітити, що цей алгоритм – простий. Така задача може змусити попрацювати дві години там, де заплачено за один, але вона навряд чи зірве дедлайн.
На жаль, цей алгоритм застарів. Відтоді, як він востаннє працював, на Amazon як мінімум двічі змінювалася захист від автоматичної авторизації. Тепер там на восьмому кроці перед відправкою форми відбувається зміна її даних скриптом, так що простим аналізом http-трафіку і складанням запитів проблема не вирішується. Після таких змін тема автоматичної авторизації на Amazon поступово переміщається з аматорських форумів на професійні біржі фрілансу.
Важко сказати, коли і як захист Amazon зміниться знову, але точно можна сказати, що це неодмінно станеться. Тому, при розгляді даної задачі конкретний робочий код менш цінний, ніж розуміння відповідних підходів та інструментів.
Короткий огляд методів
Всі методи рішення подібних завдань можна умовно розділити на три категорії:
  1. Імітація браузера: виконання запитів на основі даних отриманих хакерськими методами, такими як аналіз трафіку, реверс-інжиніринг скриптів і так далі.
  2. Автоматичне використання браузера. Сюди відноситься управління з скрипта цими браузерами (наприклад, Chrome) через спеціальний API (наприклад, з допомогою Selenium WD), а також використання headless-браузерів (наприклад PhantomJS).
  3. Використання браузера вручну. Це не обов'язково означає цілковиту відмову від автоматизації скрейпинга, але передбачає, що живий оператор буде бачити реальні сторінки і виконувати на них реальні дії через користувальницький інтерфейс.
Вибір з цих трьох пунктів – не очевидний і як мінімум суб'єктивний. Всі три категорії мають свої плюси і мінуси і кожна з них дозволяє розв'язати нашу задачу, так що розглянемо їх по черзі.
Імітація браузера
Щось подібне описано у двох попередніх статтях. Щоб здійснити скрейпинг ми відправляємо на сервер правильні http-запити. «Правильні» – це такі, які відправляв би браузер, якби скрейпинг проводився повністю вручну. Щоб з'ясувати, що саме відправляти в кожному окремому запиті, ми аналізуємо заголовки запитів та відповідей у браузері, а також дивимося исходники сторінок. Завдання цієї статті нічим принципово не відрізняється від двох попередніх, за винятком одного моменту: в даному випадку поняття «исходники сторінок» означає ще і підключені скрипти. Нам потрібно зрозуміти, яка частина коду додає дані до запитів, звідки ці дані беруться і так далі. Звичайний реверс-інжиніринг стосовно скриптам сайту.
Самий головний плюс такого підходу – його універсальність. Якщо захист проходиться стандартної зв'язкою з людини і браузера, то алгоритм її проходження може бути знайдений реверс-інжиніринг. Теоретично, винятків з цього правила немає. У світі веб-скрейпинга існують завдання, які можуть бути вирішені виключно реверс-інжиніринг.
Головний мінус такого підходу – його необмежена трудомісткість. Це шлях для сильних духом і не особливо обмежених за часом. У реальному житті реверс-інжиніринг одного складного сайту може зайняти час, за який можна написати скрипти для скрейпинга тисячі звичайних сайтів. В теорії виробник захисту може витратити на неї скільки завгодно часу, а його кваліфікація може вимагати, щоб реверс-інжиніринг займався досвідчений і талановитий хакер. Простіше кажучи, на певному рівні складності замовнику стає вигідніше замість крутого хакера найняти клерка-копипейстера. Або взагалі відмовитися від замовлення і обійтися без цих даних.
Варто відзначити, що не кожен скрейпер взагалі має хакерські навички або має в команді хакера. Зустрівшись з необхідністю розібратися в скриптах на сайті більшість відмовляється від замовлення або вибирає метод з іншої категорії. Ну, або наймає іншого професіонала.
У разі, коли алгоритм захисту не проглядається при аналізі трафіку і HTML – варто почати з інших підходів. Якщо ж алгоритм може несподівано змінитися в будь-який момент (як на Amazon), я рекомендую реверс-інжиніринг розглядати в останню чергу. До того ж мені здається, що якщо код проходження захисту Amazon, отриманий реверс-інжиніринг, викласти, наприклад, на Хабр, то можна відразу поруч написати, що цей код застарів – це швидко стане правдою.
У цій статті приклади коду будуть описувати інші методи.
Автоматичне використання браузера
Цей підхід популярний у початківців скрейперов настільки, що вони переходять до нього щоразу, як стикаються з труднощами при аналізі сайту. Професіонали з жорсткими дедлайнами також люблять цей підхід.
Головний плюс такого підходу – простота проходження скриптовой захисту, так як в скриптах сайту немає необхідності розбиратися – вони просто виконуватися в браузері і зроблять все те ж саме, як якщо б їх не запускав ваш код, а користувач з мишкою і клавіатурою. Ваш код буде нагадувати інструкції для недалекого клерка-копіпаста. Він буде легко читатися і правиться при необхідності.
Головний мінус цього підходу – ви запускаєте чужий код у себе на комп'ютері. Цей код може, наприклад, містити додатковий захист від автоматизації самого різного типу від детекторів headless-браузерів і до інтелектуального аналізу поведінки користувачів. Звичайно, можна використовувати, наприклад, proxy-сервер, який підміняє скрипт з детектором, але це зажадає того ж самого реверс-інжинірингу, а тоді взагалі незрозуміло навіщо возитися з браузерами.
(Примітка: приклад технології аналізу поведінки користувача можна подивитися на сайті AreYouaHuman, а про детектори headless-браузерів можна більше дізнатися з відмінної презентації, посилання на яку kirill3333 залишив у коментарі до позаминулого статті).
Ще один мінус цього підходу – браузери споживають набагато більше ресурсів, ніж звичайні скрейперские скрипти. Для скрейпинга тисяч (а тим більше – мільйонів) сторінок це погана ідея. Наш замовник хоче модуль, код якого буде виконуватися один раз і відправляти всього декілька запитів, так що нам про ресурси можна не турбуватися.
Для автоматичного використання браузера з скрипта на Node.JS є багато популярних інструментів. Тут я коротко проходжу по основним (якщо я кого-то дарма забув – підкажіть в коментарях):
Selenium WD – найвідоміший і найпопулярніший інструмент для автоматичного використання браузера. Не потребує додаткового представлення. В мережі є маса прикладів і рад. Кожному, хто хоча б подумує професійно зайнятися скрейпингом, варто спробувати Selenium WD.
На жаль, для рішення нашої задачі Selenium WD погано підходить. Щоб з ним працювати знадобиться встановити і налаштувати Java, сам Selenium WD (додаток Java), браузер, драйвер браузера і модуль для доступу до Selenium WD з обраної мови програмування (в нашому випадку – з Node.js). Це не найпростіший шлях, враховуючи, що нашому замовнику потрібен простий модуль на Node.js, працює «з коробки» (ну, в крайньому випадку вимагає щось на зразок '
npm install
').
PhantomJS – headless-браузер на движку WebKit. Він запускає сайти в реальному браузері, тільки не показує користувачеві сторінок, а замість інтерфейсу використовує скрипти на Javascript. Браузер PhantomJS настільки повноцінний, що для нього навіть є драйвер для Selenium WD і ця зв'язка відмінно працює.
З точки зору нашої задачі головне, що потрібно знати про PhantomJS це те, що хоч він і використовує той же Javascript що і Node.js, але це НЕ Node.js. Схожий, але не те. Можна зробити модуль на Node.js, який би безпосередньо використовував PhantomJS. Можна зробити окремий скрипт на PhantomJS, а для нього зробити модуль-обгортку, який буде запускати окремий процес і отримувати дані через
stdout
. У найпростішому випадку це виглядає так:
var sys = require('sys'),
exec = require('child_process').exec;

module.exports = function(callback) {
exec('phantomjs script.js', function(err, stdout){
callback(err === null ? stdout : false);
});
};

Щоб не возитися з
stdout
і працювати через зручний інтерфейс має сенс використовувати одну з готових (і ретельно ким-то вже протестованих) обгорток над PhantomJS.
SlimerJS – грубо кажучи, це той же PhantomJS, тільки не на WebKit, а на Gecko. В нашому випадку важливо, що SlimerJS підтримується меншою кількістю обгорток ніж PhantomJS.
CasperJS – найвідоміша Node.js-обгортка над PhantomJS і SlimerJS. Про те, чому CasperJS краще ніж голий PhantomJS добре написано, наприклад, цієї статті (англійською). А ось в цієї статті (теж англійською) можна подивитися приклад проходження авторизації на Amazon за допомогою CasperJS. Цілком робочий рішення, але це не кращий варіант (оцініть, хоча б, обсяг коду в прикладі). Я рекомендую замість CasperJS звернути увагу на наступний пункт списку:
Horseman.js – дуже гарна обгортка над PhantomJS. Ідеальний варіант для написання модулів зразок нашого. Не вимагає ніяких складних дій з інсталяції та налаштування. Достатньо в нашому модулі додати в залежності
phantomjs-prebuilt
та
node-horseman
і все. Інтерфейс Horseman.js гранично лаконічний, гнучкий і легкий для читання. Ось так буде виглядати робочий код нашого модуля, написаний з використанням Horseman.js:
var Horseman = require('node-horseman');
var horseman = new Horseman();

var startURL = 'https://www.amazon.com/gp/css/homepage.html/';

module.exports = function(email, password, work){
horseman
.userAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36')
.open(startURL)
.click('#nav-tools>a')
.waitForNextPage()
.type('form [name= "email"]', email)
.type('form [name=password]', password)
.click('#signInSubmit')
.waitForNextPage()
.cookies()
.then((cookies)=>{
work(cookies);
})
.close();
}

Такий код можна читати не дивлячись в документацію. І він працює.
NW – ще один керований браузер (колишній node-webkit). Дозволяє управляти движком Blink з скриптів на Node.js. Основне призначення – створення десктопних додатків з використанням веб-технологій, але може працювати і як headless. На NW можна робити повноцінні програми для скрейпинга. Наприклад, такі, як от в цієї статті vmb. Такі інструменти, як NW, особливо гарні, коли замовник боїться командного рядка і хоче віконце з кнопками. У нашому випадку NW не кращий вибір.
Electron – ще один інструмент для створення десктопних додатків на базі веб-технологій. По суті це молодий і перспективний конкурент NW і з його допомогою також можна робити програми для скрейпинга з відмінною індикацією. Крім того, Electron легко можна використовувати в якості headless-браузера, але підключати його у наш модуль краще не безпосередньо, а через обгортку.
Nightmare – найвідоміша обгортка над Electron. Незважаючи на назву справляє дуже гарне враження. Спроектований настільки гладко, що деякі початківці скрейперы примудряються використовувати Nightmare взагалі нічого не знаючи про Electron. По інтерфейсу дуже схожий на Horseman.js (і майже так само гарний). Ось так буде виглядати робочий код нашого модуля, написаний з використанням Nightmare:
var Nightmare = require('nightmare');
var nightmare = Nightmare()

var startURL = 'https://www.amazon.com/gp/css/homepage.html/';

module.exports = function(email, password, work){
nightmare
.goto(startURL)
.click('#nav-tools>a')
.wait('#signInSubmit')
.type('form [name= "email"]', email)
.type('form [name=password]', password)
.click('#signInSubmit')
.wait('#nav-tools')
.cookies.get()
.end()
.then(function (cookies) {
work(cookies);
})
.catch(function (error) {
console.error('Authorization failed:', error);
});
}

В принципі тут все те ж саме, що і у випадку з Horseman.js. Під капотом Electron замість PhantomJS, але це в очі не впадає.
Chimera – звичайний модуль для Node.js з мінімумом залежностей (фактично тільки
request
). Він дозволяє завантажити сторінку, виконати скрипт в її контексті і обробити його результат у контексті Node.js. Серед тих, хто прийшов у скрейпинг з фронтенда, зустрічаються любителі цього модуля, але на мій погляд він дуже мінімалістичний. Принаймні, якщо замовник упреться і зажадає модуль без додається до нього браузерного движка, то можна використовувати не Chimera, а наступний варіант:
ZombieJS – це модуль часто позиціонується як аналог PhantomJS, який можна запускати прямо з Node.js. Жодних браузерних движків ZombieJS не використовує, обходячись такими звичними інструментами, як
request
,
jsdom
або
ws
. Порівняно з Horseman.js код на ZombieJS виглядає громіздким, але після Chimera це зразок зручності і лаконічності:
var Safari = require('гра');
var safari = new Browser();

var startURL = 'https://www.amazon.com/gp/css/homepage.html/';

module.exports = function(email, password, work){
browser.visit(startURL, function(){
browser.clickLink('#nav-tools>a', function() {
browser.fill('email', email);
browser.fill('password', password);
browser.pressButton('#signInSubmit', function(){
var cookies = browser.saveCookies().split('\n');
var newCookies = {};
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].split(';')[0].split('=');
newCookies[cookie[0]] = cookie[1];
}
browser.tabs.closeAll();
work(cookies);
});
});
});
}

До явних мінусів ZombieJS можна віднести погану документацію. Раніше вона була гарна, але потім розробник її сильно змінив в гіршу сторону. Не буду будувати конспірологічних теорій про те, навіщо він це зробив, але ось вам пара посилань, за якими з ZombieJS можна розібратися: приклади і API.
Цікаво, що PhantomJS, Electron і ZombieJS багатьма сайтами сприймаються по-різному, так що один інструмент проходить захист, а інший – блокується. Це серйозний привід, щоб бути готовим скористатися будь-яким з цих трьох інструментів. Хоча, звичайно, такі складні випадки зустрічаються рідко. З сайтом Amazon, наприклад, працюють всі три скрипта. Ну… поки працюють.
Використання браузера вручну
Може здатися, що цей пункт тут наводиться заради приколу, однак це не так. Бувають ситуації, коли такий підхід не просто виправданий, але й оптимальним. Наприклад, коли потрібно один раз виконати складне для автоматизації дію, а потім дуже багато разів автоматично виконувати щось просте. Наприклад, авторизуватися на Amazon, а потім довго скрейпить його.
Головний мінус такого підходу – необхідність в живій користувача для кожного запуску скрипта. Тобто не можна просто стартувати скрейпинг
cron
, потрібно щоб хтось дивився на сторінки і що з ними робив.
Головний плюс такого підходу – наочність процесу. Користувач бачить що відбувається на сторінці і помітить, якщо щось піде не так. А можливо він зможе все виправити. Наприклад, якщо сайт раптово почне питати капчу (як це робить Amazon, якщо його довго напружувати автоматичною авторизацією).
найпростіший спосіб додати до свого скрипту використання браузера вручну – це написання інструкції для користувача. Зрозумілий текст, що пояснює, наприклад, як взяти куки браузера і помістити в текст сценарію – це та ж програма, тільки не для комп'ютера, а для людини. Багато програмісти вміють писати такі програми добре, а багато замовників легко йдуть на такий компроміс щоб заощадити час і гроші. На жаль, нашому замовнику потрібен модуль, що не вимагає користувальницьких інструкцій.
Ще один спосіб – написати власний браузер, який буде запускатися тільки заради авторизації на Amazon, а потім закриватися. Наприклад, на основі Electron. Це не так вже й складно зробити. Якщо в нашому скрипті на Nightmare внести невелику зміну, то скрипт буде показувати сторінки, з якими працює. Користувач буде бачити, як скрипт заповнює поля форми і відправляє її. Потрібно просто ініціалізувати Nightmare з додатковим параметром. Ось так:
var nightmare = Nightmare({ show: true })

Наприклад, так можна побачити, що сайт почав запитувати капчу, що відбувається не так вже рідко. Більш того, користувач може вводити капчу прямо у вікні Electron і все буде працювати. Тільки час очікування в скрипті варто поставити побільше, а то користувач може не встигнути. Взагалі, можна прибрати автоматичне заповнення полів форми і надати це користувачеві. Тоді можна буде не зберігати пароль в тексті скрипта. Більш того, тоді можна буде у різні моменти часу скрейпить від імені різних користувачів.
З іншого боку, Nightmare можна навчити розпізнавати запит капчі і проходити її за допомогою таких сервісів, як Death by Captcha, але у нас в задачі такої вимоги немає.
Висновок
Завдання з цієї статті набагато складніше, ніж все те, що зазвичай роблять веб-скрейперы в реальному житті. Однак на сучасних біржах фріланса часто буває так, що битву за замовлення виграє той, хто першим заявляє, що здатна його виконати швидко і добре. В результаті дуже вигідно мати здатність брати замовлення практично не дивлячись і не потрапляти в неприємності з зірваними дедлайнами, проваленими угодами, зіпсованою репутацією і понизившимся рейтингом. Тому, навіть якщо скрейпер цілими днями робить елементарні скрипти для отримання даних з простих сайтів, корисно бути готовим зіткнутися зі складною захистом і не злякатися. Якщо хоча б поверхово освоїти підходи, описані в цій статті (і в двох попередніх), то відсоток «нерозв'язних» замовлень буде зневажливо малий.
найближчим часом я планую написати про скрейпинге регулярно оновлюваних даних, скрейпинге даних великих обсягів, термінології скрейпинга, а також про моральних і правових аспектах скрейпинга. Поради та рекомендації щодо вибору тем особливо вітаються.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.