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

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


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

Спасибо за внимание!
Поделиться с друзьями
-->

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


  1. EreminD
    27.06.2017 14:27

    хочу пример
    «как хотели» и «как получилось, потому что, оказывается, вот что...»


    1. vshvydky
      27.06.2017 17:29

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


  1. maksym1up
    27.06.2017 17:29

    Не нашел отличий от Phantomjs/Casperjs


    1. vshvydky
      27.06.2017 17:30

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


    1. Large
      28.06.2017 01:00
      -1

      поддержка es2015+


  1. faiwer
    28.06.2017 07:11
    -1

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


    1. vshvydky
      28.06.2017 09:27
      +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


      1. faiwer
        28.06.2017 10:18

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


        1. vshvydky
          28.06.2017 10:56

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

          let text = await nightmare.evaluate((selector)=>{ rerueen document.querySelector(selector).textContent ;}, '#elem-id'); 
          console.log(text);
          

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


          1. faiwer
            28.06.2017 11:11

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


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

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


            1. vshvydky
              28.06.2017 11:34

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


              1. faiwer
                28.06.2017 16:13

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


        1. arvitaly
          29.06.2017 10:26

          По другому используйте electron без оберток, он позволяет подгружать на страницу обычные nodejs-модули, да и возможностей контролировать ситуацию гораздо больше чем у nightmare, все таки это библиотека для тестирования, а не ботописания.


  1. mihmig
    28.06.2017 09:16

    Как прикажете «дожидаться загрузки DOM» например в богомерзком личном кабинете МТС — там асинхронно загружается порядка 10 кусочков (гудки, подписки и прочая муть) страницы? Причём самая важная информация может тупо не загрузиться — пользователь должен нажать малюсенькую иконку «Обновить».


    1. vshvydky
      28.06.2017 09:32

      если интересует появление конкретного элемента, то достаточно сделать wait(cssSelector) и оно будет ждать его появления до таймаута, заданного при инициализации (дефолт 30 сек), если не дождется выплюнет исключение.
      Если селектор есть изначально, и нужно отловить данные, можно сделать .wait(fn[, arg1, arg2,...])
      Пример с тестов:

              .wait(function () {
                var text = document.querySelector('a').textContent;
                return (text === 'A');
              });
      

      Получить наличие значения или его отсутствие можно через return !!text


    1. pan-alexey
      29.06.2017 11:11

      К примеру, модно заинжектить свой код, в котором все ajax оборачиваются в функцию обертку (простите за тавтологию).


      1. vshvydky
        01.07.2017 00:17

        Да верно. Любой код, к которому нет доступа напрямую.


  1. diman24
    28.06.2017 22:18

    Спасибо, не знал про Electron. А то PhantomJS уже совсем старичок, да.
    Правда с недавних пор (версия 59 для Mac и Linux, и 60 для Windows) Chrome начал нативно поддерживать headless browsing (можно найти по ключевым словам 'Getting Started with Headless Chrome'), так что есть мнение, что надо начинать больше в ту сторону смотреть…