Тестирование или парсинг сайтов с динамическим дом и многое другое. Nightmare.js — ему все равно

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

В двух словах:


1. Пакет можно использовать для тестирования сайтов.
2. Пакет можно использовать для парсинга данных.
3. Пакет можно использовать для автоматизации ввода данных на сайты.

Альтернативы:


Casper.js, phantom.js, watir и много кто еще, в гугле полно всех и вся. Почему я за nightmare.js:

  1. Простота использования.
  2. Полная поддержка html5, никаких конфликтов с сайтами.
  3. Расширяемый через экшены.

Структура библиотеки


Nightmare класс использует фреймворк electron, для каждой страницы создавая объект (BrowserWindow) который запускает браузер оболочку Chromium.

Принцип работы


  1. Nightmare инициализирует новое приложение electron с стартовой страницей, которую необходимо подвергнуть дальнейшей обработке.
  2. Перед загрузкой исследуемой страницы загружаются скрипты, которые позволяют поддерживать двустороннее взаимодействие программиста и страницы через серию эмиттеров.
  3. Nightmare предоставляет программисту набор апи (цепочки действий), позволяющие произвести любые манипуляции с сайтом и получить требуемые данные.

Плюсы


  1. Код на стороне клиента и сайта написан на одном языке, никаких шаблонизаторов не требуется.
  2. Возможность расширять модулями через создание экшенов. Экшен может создаваться на уровне класса nightmare или на уровне класса nightmare и уровне electron (что в свою очередь дает возможность использовать devapi Chromium). В npm уже достаточно готовых модулей расширений, которые можно подключать к себе в проект (например realMouse полностью эмулирующую наведение мыши или работа с ифреймами, что блокируется безопасностью браузера).
  3. Все команды являются цепочками, каждая из которых возвращает промис, это позволяет писать код как в стиле промисов, так и внутри асинк функций или генераторов.
  4. Относительно небольшая нагрузка на процессор и память, нужно помнить, что сравнивать такой инструмент с простыми гет и пост запросами не этично, по скорости и памяти браузерные парсеры проигрывают без вариантов).
  5. Работа nightmare возможна в двух режимах, режим отображения браузера и режим фонового процесса.
  6. Поддерживает прокси. Установка юзерагента, выставление расширения браузера.
  7. Можно включать или отключать отображение изображений, поддержку webGL и еще кучу всего.
  8. Можно создавать прелоад скрипты, что позволяет добавлять на страницу до загрузки свои функции, библиотеки. Как частный пример можно переписать функцию addEventListener сделав ее декоратором для реальной + инжектировать аналитические функции для проверки того. Что в действительности делает сайт, когда вы на нем находитесь или бороться с навязчивостью фингер принт, который столь сильно полюбили все кому не лень, забывая о вашей «анонимности».

От эмоций к делу


Классический пример использования модуля из документации:

var Nightmare = require('nightmare');		
var nightmare = Nightmare({ show: true });

nightmare
  .goto('https://duckduckgo.com')
  .type('#search_form_input_homepage', 'github nightmare')
  .click('#search_button_homepage')
  .wait('#zero_click_wrapper .c-info__title a')
  .evaluate(function () {
    return document.querySelector('#zero_click_wrapper .c-info__title a').href;
  })
  .end()
  .then(function (result) {
    console.log(result);
  })
  .catch(function (error) {
    console.error('Search failed:', error);
  });

В двух словах о происходящем


Подключение библиотеки, создание объекта с режимом видимого браузера. Заход на страницу, поиск элемента по ЦСС селектору, ввод текста, нажатие кнопки, ожидание появления нового цсс сетектора, выполнение функции на стороне бразуера и возвращение ее, после завершения цепочки заданий в then будет передан результат работы или сработает исключение. На мой взгляд все просто и удобно, но как только скрипт обхода страницы становится большим, такое описание команд становится неудобным, потому предлагаю хороший вариант использования в асинк функции:

const Nightmare = require('nightmare');		

(async ()=>{
let nightmare; 
try {
	nightmare = Nightmare({ show: true });
	await nightmare
  		.goto('https://duckduckgo.com')
 		 .type('#search_form_input_homepage', 'github nightmare')
		  .click('#search_button_homepage')
		  .wait('#zero_click_wrapper .c-info__title a');

	let siteData = await nightmare.evaluate(function () {
    		return document.querySelector('#zero_click_wrapper .c-info__title a').href;
  		});
	// последующая работа с данными
} catch (error) {
	console.error(error);
	throw error;
} finally {
	await nightmare.end();
}
})();

В чем преимущества такого варианта написания кода? Можно получать сколько угодно раз данные с сайта через evaluate, анализировать их и применять различные поведенческие сценарии, описывая это в вашем скрипте.

Можно последовательно переходить по страницам через await nightmare.goto(….), при том Nightmare будет дожидаться загрузки дом.

О задокументированных возможностях


Описывать все функции в примерах считаю бессмысленным, так как все это хорошо указано в документации. Скажу лишь то, что модуль умеет считывать любые данные, делать скриншоты, сохранять html страницы, pdf страницы, передавать на сайт данные. Через доп модули доступна загрузка файлов на сервер через form input type=”file”. Умеет реагировать на alert, prompt, confirm, может транслировать в виде событий данные из консоли.

Какие особенности стоит учитывать при работе с nightmare


Нужно понимать, что каждое действие будет либо совершено либо произойдет выброс исключительной ситуации, а потому в местах, где нет уверенности, что код пройдет 100% нужно обертывать запросы в try catch и обрабатывать из соответственно. Как пример wait(selector) данная инструкция даст команду приостановить выполнение скрипта до появления html элемента с соответствующим цсс селектором, но в модуле есть дефолтный таймаут, его можно изменять опционально, при наступлении которого будет выброшено исключение, соответственно можно будет обработать почему на странице нет чего-либо и как-то на это среагировать.

Резюме


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

Ссылки


Nigthmare.js
Electron

Спасибо за внимание!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 19

    0
    хочу пример
    «как хотели» и «как получилось, потому что, оказывается, вот что...»
      0
      пример от задачи зависит, самое креативное, что приходилось делать, это инжектирование на страницу до загрузки кода декоратора addEventListener, который логирует все листенеры и оборачивает передаваемую функцию в доп логгер, далее отлов событий через внутренние коммуникации и соответствующие реакции уже на загруженном сайте. Плюсы очевидны, таким образом можно добраться и контролировать код, который активируется через анонимные самозапускающиеся функции и в обычном случае к нему доступа нет.
      0
      Не нашел отличий от Phantomjs/Casperjs
        0
        даже спорить не буду, я в начале статьи написал про аналоги. Для меня плюсы простота в использовании, использование хромиума, модульность, прозрачность кода.
          –1
          поддержка es2015+
          –1

          Не могли бы вы вкратце описать принцип работы evaluate? Метод сериализуется через toString и передаётся на сторону electron-а, где eval-ится? Т.е. всё также как и в casper, slimer?

            +1
            evalute -> evalute_now -> child.call('javascript', source, done) передается код функции как строка, под капотом call эмит в чилд процесс с контролем доставки -> parent.respondTo('javascript'… -> win.webContents.executeJavaScript(src);
            эта цепочка работает с очередью и возвращает в прмис данные.
            Любой метод типа .click .type .wait можно рассматривать как частный случай работы evaluate, можно посмотреть в lib/actions.js
            Отдельно про передачу функции:
            var source = template.execute({ src: String(js_fn), args: args});
            this.child.call('javascript', source, done);
            под капотом модуль minstache, с шаблоном, куда инжектится функция (как текст)
            смотри файл lib/javascript.js
              0

              Спасибо. Не ожидал такого подробного ответа. Мне бы хватило простого "да" ) Спросил потому, что у casper.js (slimer, phantom) примерно такой же подход. Очень неудобно. Но, похоже, ввиду того, что по другому никак, с этим приходится мириться.

                0
                я отношусь к этому как к транспорту, на счет неудобства, оно же под капотом, о нем просто надо знать и не более того. Получить данные со страницы просто.
                let text = await nightmare.evaluate((selector)=>{ rerueen document.querySelector(selector).textContent ;}, '#elem-id'); 
                console.log(text);
                

                по сути транспорт связывает код в исполняемом файле с кодом на обрабатываемой странице, никаких вермешелей коллбеков не требуется, все читабельно и просто.
                Но тут опять же на вкус и цвет фломастеры разные.
                  0

                  В идеальном случае же:


                  • можно было бы использовать ту версию JS, коей располагает nodeJS, а не, скажем, es5. Правда тут можно прикрутить что-нибудь вроде babelEvaluate
                  • stack-trace-ы ошибок в evaluate-методе показывали не какую-то бесполезную муть, а конкретную строку в тесте. Тут в принципе тоже можно пошаманить с console.trace на уровне самого Nightmare.
                  • возможность использовать замыкания, а не дублировать одно и тоже дважды. Но это скорее месты. Не вижу возможности такое реализовать вовсе.
                  • иметь возможность передать внутрь несериализуемые данные (к примеру set-ы и map-ы).

                  Ещё у меня был ряд сложностей с undefined-null-NaN. Кажется там всё перегонялось в JSON, и slimer поступал с ними не так, как phantom. Кто-то из ужасно коверкал данные по пути. Постепенно тесты всё больше и больше покрывались набором костылей. Часто получал ошибки вроде circular reference или что-то в таком духе, если куда-нибудь попадали несериализуемые данные.

                    0
                    ошибки в evalute можно так же контролировать
                    ()=>{
                    try {} catch(error) { return что-то-что-подверглось_анализу}
                    }
                      0

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

                  0
                  По другому используйте electron без оберток, он позволяет подгружать на страницу обычные nodejs-модули, да и возможностей контролировать ситуацию гораздо больше чем у nightmare, все таки это библиотека для тестирования, а не ботописания.
              0
              Как прикажете «дожидаться загрузки DOM» например в богомерзком личном кабинете МТС — там асинхронно загружается порядка 10 кусочков (гудки, подписки и прочая муть) страницы? Причём самая важная информация может тупо не загрузиться — пользователь должен нажать малюсенькую иконку «Обновить».
                0
                если интересует появление конкретного элемента, то достаточно сделать wait(cssSelector) и оно будет ждать его появления до таймаута, заданного при инициализации (дефолт 30 сек), если не дождется выплюнет исключение.
                Если селектор есть изначально, и нужно отловить данные, можно сделать .wait(fn[, arg1, arg2,...])
                Пример с тестов:
                        .wait(function () {
                          var text = document.querySelector('a').textContent;
                          return (text === 'A');
                        });
                

                Получить наличие значения или его отсутствие можно через return !!text
                  0
                  К примеру, модно заинжектить свой код, в котором все ajax оборачиваются в функцию обертку (простите за тавтологию).
                    0
                    Да верно. Любой код, к которому нет доступа напрямую.
                  0
                  Спасибо, не знал про Electron. А то PhantomJS уже совсем старичок, да.
                  Правда с недавних пор (версия 59 для Mac и Linux, и 60 для Windows) Chrome начал нативно поддерживать headless browsing (можно найти по ключевым словам 'Getting Started with Headless Chrome'), так что есть мнение, что надо начинать больше в ту сторону смотреть…
                    0
                    столкнулся с одним сайтом, не буду упоминать их в суе, вдруг напрягутся. Так вот их сайт не загружается в кошмаре, висит на инициализации, не стал вникать почему, но это стало причиной попробовать хедлесс хром, и что я могу сказать, штука очень даже достойная.
                    Если сравнивать с кошмаром, конечно там можно прокидывать низкоуровневые запросы к девтулс хрома, но делается это через экшен кошмара + экшен электрона + вызов обсуждаемых функций.
                    Плюсы, которые я сразу в хеддлес хром для себя отметил, есть возможность активировать подписки эмиттеров сетевых сообщений, будь то post get запросы к бекэнду или активность на поднятых сокетах, использование авным образом всего апи девтулс.
                    Минусы, все манипуляции с браузером достаточно низкоуровневые, а потому если требуется что-то быстрое типа 3 кликов и забыть, кошмар явно в выигрыше, а вот если нужно что-то «извращенное», есть смысл посмотреть к хедлессу. Не исключено, что кошмар рано или поздно посмотрят на эту возможность и сделают свое высокоуровневое апи под хеддлес. Время покажет.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое