phantomjs не ждет полной загрузки страницы

137

Я использую PhantomJS v1.4.1 для загрузки некоторых веб-страниц. У меня нет доступа к их серверной части, я просто получаю ссылки, указывающие на них. Я использую устаревшую версию Phantom, потому что мне нужно поддерживать Adobe Flash на этих веб-страницах.

Проблема в том, что многие веб-сайты загружают свой незначительный контент асинхронно, и поэтому обратный вызов Phantom onLoadFinished (аналог onLoad в HTML) срабатывает слишком рано, когда еще не все загружено. Кто-нибудь может подсказать, как мне ждать полной загрузки веб-страницы, чтобы сделать, например, снимок экрана со всем динамическим контентом, таким как реклама?

nilfalse
источник
3
Думаю,
пришло

Ответы:

76

Другой подход - просто попросить PhantomJS немного подождать после загрузки страницы перед выполнением рендеринга, как в обычном примере rasterize.js , но с более длительным тайм-аутом, чтобы JavaScript мог завершить загрузку дополнительных ресурсов:

page.open(address, function (status) {
    if (status !== 'success') {
        console.log('Unable to load the address!');
        phantom.exit();
    } else {
        window.setTimeout(function () {
            page.render(output);
            phantom.exit();
        }, 1000); // Change timeout as required to allow sufficient time 
    }
});
rhunwicks
источник
1
Да, в настоящее время я придерживался этого подхода.
nilfalse
102
Это ужасное решение, извините (это вина PhantomJS!). Если вы ждете целую секунду, но загрузка занимает 20 мс, это пустая трата времени (например, пакетные задания), или если это занимает больше секунды, это все равно не удастся. Такая неэффективность и ненадежность невыносимы для профессиональной работы.
CodeManX
9
Настоящая проблема здесь в том, что вы никогда не знаете, когда javascript завершит загрузку страницы, а браузер также не знает об этом. Представьте себе сайт, который имеет некоторый JavaScript, загружающий что-то с сервера в бесконечном цикле. С точки зрения браузера - выполнение javascript никогда не заканчивается, так что в этот момент вы хотите, чтобы фантомы сообщили вам, что оно закончилось? Эта проблема неразрешима в общем случае, за исключением случаев ожидания решения по тайм-ауту и ​​надежды на лучшее.
Максим Галушка
5
Это все еще лучшее решение на 2016 год? Кажется, что мы должны быть в состоянии сделать лучше, чем это.
Адам Томпсон
6
Если вы контролируете код, который пытаетесь прочитать, вы можете явно вызвать обратный вызов фантома: phantomjs.org/api/webpage/handler/on-callback.html
Энди Смит
52

Я бы предпочел периодически проверять document.readyStateстатус ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Хотя этот подход немного неуклюж, вы можете быть уверены, что внутри onPageReadyфункции вы используете полностью загруженный документ.

var page = require("webpage").create(),
    url = "http://example.com/index.html";

function onPageReady() {
    var htmlContent = page.evaluate(function () {
        return document.documentElement.outerHTML;
    });

    console.log(htmlContent);

    phantom.exit();
}

page.open(url, function (status) {
    function checkReadyState() {
        setTimeout(function () {
            var readyState = page.evaluate(function () {
                return document.readyState;
            });

            if ("complete" === readyState) {
                onPageReady();
            } else {
                checkReadyState();
            }
        });
    }

    checkReadyState();
});

Дополнительное объяснение:

Использование setTimeoutвместо вложенных setIntervalпредотвращает checkReadyState«наложение» и условия гонки, когда его выполнение затягивается по некоторым случайным причинам. setTimeoutимеет задержку по умолчанию 4 мс ( https://stackoverflow.com/a/3580085/1011156 ), поэтому активный опрос не окажет существенного влияния на производительность программы.

document.readyState === "complete"означает, что документ полностью загружен всеми ресурсами ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

Матеуш Чаритонюк
источник
4
комментарий на setTimeout против setInterval - это здорово.
Гал Браха
1
readyStateсработает только после полной загрузки DOM, однако все <iframe>еще могут загружаться любые элементы, поэтому он не отвечает на первоначальный вопрос
CodingIntrigue
1
@rgraham Это не идеально, но я думаю, что мы можем сделать так много только с этими рендерами. Будут крайние случаи, когда вы просто не будете знать, загружено ли что-то полностью. Подумайте о странице, где контент задерживается нарочно на одну или две минуты. Неразумно ожидать, что процесс рендеринга будет сидеть сложа руки и ждать неопределенное количество времени. То же самое касается контента, загружаемого из внешних источников, который может быть медленным.
Брэндон Эллиотт
3
Это не учитывает загрузку JavaScript после полной загрузки DOM, например с Backbone / Ember / Angular.
Адам Томпсон
1
У меня не сработало. readyState complete, возможно, вполне сработало, но на этом этапе страница была пустой.
Стив Стейпл
21

Вы можете попробовать комбинацию примеров waitfor и rasterize:

/**
 * See https://github.com/ariya/phantomjs/blob/master/examples/waitfor.js
 * 
 * Wait until the test condition is true or a timeout occurs. Useful for waiting
 * on a server response or for a ui change (fadeIn, etc.) to occur.
 *
 * @param testFx javascript condition that evaluates to a boolean,
 * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
 * as a callback function.
 * @param onReady what to do when testFx condition is fulfilled,
 * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
 * as a callback function.
 * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
 */
function waitFor(testFx, onReady, timeOutMillis) {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s
        start = new Date().getTime(),
        condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()), //< defensive code
        interval = setInterval(function() {
            if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                // If not time-out yet and condition not yet fulfilled
                condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
            } else {
                if(!condition) {
                    // If condition still not fulfilled (timeout but condition is 'false')
                    console.log("'waitFor()' timeout");
                    phantom.exit(1);
                } else {
                    // Condition fulfilled (timeout and/or condition is 'true')
                    console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                    typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
                    clearInterval(interval); //< Stop this interval
                }
            }
        }, 250); //< repeat check every 250ms
};

var page = require('webpage').create(), system = require('system'), address, output, size;

if (system.args.length < 3 || system.args.length > 5) {
    console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
    console.log('  paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
    phantom.exit(1);
} else {
    address = system.args[1];
    output = system.args[2];
    if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
        size = system.args[3].split('*');
        page.paperSize = size.length === 2 ? {
            width : size[0],
            height : size[1],
            margin : '0px'
        } : {
            format : system.args[3],
            orientation : 'portrait',
            margin : {
                left : "5mm",
                top : "8mm",
                right : "5mm",
                bottom : "9mm"
            }
        };
    }
    if (system.args.length > 4) {
        page.zoomFactor = system.args[4];
    }
    var resources = [];
    page.onResourceRequested = function(request) {
        resources[request.id] = request.stage;
    };
    page.onResourceReceived = function(response) {
        resources[response.id] = response.stage;
    };
    page.open(address, function(status) {
        if (status !== 'success') {
            console.log('Unable to load the address!');
            phantom.exit();
        } else {
            waitFor(function() {
                // Check in the page if a specific element is now visible
                for ( var i = 1; i < resources.length; ++i) {
                    if (resources[i] != 'end') {
                        return false;
                    }
                }
                return true;
            }, function() {
               page.render(output);
               phantom.exit();
            }, 10000);
        }
    });
}
rhunwicks
источник
3
Похоже, что он не будет работать с веб-страницами, которые используют любую из технологий push-серверов, так как ресурс все еще будет использоваться после возникновения onLoad.
nilfalse
Есть ли какие-либо драйверы, например. Полтергейст , есть такая особенность?
Джаред Бек,
Можно ли использовать waitFor для опроса всего HTML-текста и поиска по определенному ключевому слову? Я пытался реализовать это, но кажется, что опрос не обновляется до последней загруженной HTML-источника.
fpdragon
14

Может быть , вы можете использовать onResourceRequestedи onResourceReceivedобратные вызовы для обнаружения асинхронной загрузки. Вот пример использования этих обратных вызовов из их документации :

var page = require('webpage').create();
page.onResourceRequested = function (request) {
    console.log('Request ' + JSON.stringify(request, undefined, 4));
};
page.onResourceReceived = function (response) {
    console.log('Receive ' + JSON.stringify(response, undefined, 4));
};
page.open(url);

Также вы можете посмотреть на examples/netsniff.jsрабочий пример.

Supr
источник
Но в этом случае я не могу использовать один экземпляр PhantomJS для загрузки более одной страницы одновременно, верно?
nilfalse
Относится ли onResourceRequested к запросам AJAX / Cross Domain? Или это относится только к как CSS, изображения .. и т. Д.?
CMCDragonkai
@CMCDragonkai Я никогда не использовал его сам, но на основании этого кажется, что он включает в себя все запросы. Цитата:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Супр
Я использовал этот метод для крупномасштабного рендеринга PhantomJS, и он работает довольно хорошо. Вам нужно много умов, чтобы отслеживать запросы и смотреть, если они терпят неудачу или время ожидания. Дополнительная информация: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Райан Доэрти
14

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

Хотя это решение может послужить хорошей отправной точкой, я заметил, что оно терпит неудачу, поэтому это определенно не полное решение!

Мне не очень повезло, используя document.readyState.

На меня повлиял пример waitfor.js, найденный на странице примеров phantomjs .

var system = require('system');
var webPage = require('webpage');

var page = webPage.create();
var url = system.args[1];

page.viewportSize = {
  width: 1280,
  height: 720
};

var requestsArray = [];

page.onResourceRequested = function(requestData, networkRequest) {
  requestsArray.push(requestData.id);
};

page.onResourceReceived = function(response) {
  var index = requestsArray.indexOf(response.id);
  requestsArray.splice(index, 1);
};

page.open(url, function(status) {

  var interval = setInterval(function () {

    if (requestsArray.length === 0) {

      clearInterval(interval);
      var content = page.content;
      console.log(content);
      page.render('yourLoadedPage.png');
      phantom.exit();
    }
  }, 500);
});
Дейв
источник
Дали большой палец вверх, но использовали setTimeout с 10 вместо интервала
GDmac
Вы должны проверить, что response.stage равен 'end', прежде чем удалять его из массива запросов, иначе он может быть удален преждевременно.
Реймунд
Это не работает, если ваша веб-страница загружает DOM динамически
Buddy
13

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

Используйте это после onLoadFinish ().

function onLoadComplete(page, callback){
    var waiting = [];  // request id
    var interval = 200;  //ms time waiting new request
    var timer = setTimeout( timeout, interval);
    var max_retry = 3;  //
    var counter_retry = 0;

    function timeout(){
        if(waiting.length && counter_retry < max_retry){
            timer = setTimeout( timeout, interval);
            counter_retry++;
            return;
        }else{
            try{
                callback(null, page);
            }catch(e){}
        }
    }

    //for debug, log time cost
    var tlogger = {};

    bindEvent(page, 'request', function(req){
        waiting.push(req.id);
    });

    bindEvent(page, 'receive', function (res) {
        var cT = res.contentType;
        if(!cT){
            console.log('[contentType] ', cT, ' [url] ', res.url);
        }
        if(!cT) return remove(res.id);
        if(cT.indexOf('application') * cT.indexOf('text') != 0) return remove(res.id);

        if (res.stage === 'start') {
            console.log('!!received start: ', res.id);
            //console.log( JSON.stringify(res) );
            tlogger[res.id] = new Date();
        }else if (res.stage === 'end') {
            console.log('!!received end: ', res.id, (new Date() - tlogger[res.id]) );
            //console.log( JSON.stringify(res) );
            remove(res.id);

            clearTimeout(timer);
            timer = setTimeout(timeout, interval);
        }

    });

    bindEvent(page, 'error', function(err){
        remove(err.id);
        if(waiting.length === 0){
            counter_retry = 0;
        }
    });

    function remove(id){
        var i = waiting.indexOf( id );
        if(i < 0){
            return;
        }else{
            waiting.splice(i,1);
        }
    }

    function bindEvent(page, evt, cb){
        switch(evt){
            case 'request':
                page.onResourceRequested = cb;
                break;
            case 'receive':
                page.onResourceReceived = cb;
                break;
            case 'error':
                page.onResourceError = cb;
                break;
            case 'timeout':
                page.onResourceTimeout = cb;
                break;
        }
    }
}
deemstone
источник
11

Я нашел этот подход полезным в некоторых случаях:

page.onConsoleMessage(function(msg) {
  // do something e.g. page.render
});

Если у вас есть страница, поместите в нее какой-нибудь скрипт:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
Brankodd
источник
Это выглядит как очень хороший обходной путь, однако я не смог получить ни одного сообщения журнала со своей страницы HTML / JavaScript для прохождения через phantomJS ... событие onConsoleMessage никогда не срабатывало, пока я прекрасно видел сообщения на консоли браузера, и Понятия не имею почему.
Дирк
1
Мне нужно page.onConsoleMessage = function (msg) {};
Энди Валаам
5

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

Второй аргумент - это функция обратного вызова, которая будет вызвана, как только ответ будет готов.

phantom = require('phantom');

var fullLoad = function(anUrl, callbackDone) {
    phantom.create(function (ph) {
        ph.createPage(function (page) {
            page.open(anUrl, function (status) {
                if (status !== 'success') {
                    console.error("pahtom: error opening " + anUrl, status);
                    ph.exit();
                } else {
                    // timeOut
                    global.setTimeout(function () {
                        page.evaluate(function () {
                            return document.documentElement.innerHTML;
                        }, function (result) {
                            ph.exit(); // EXTREMLY IMPORTANT
                            callbackDone(result); // callback
                        });
                    }, 5000);
                }
            });
        });
    });
}

var callback = function(htmlBody) {
    // do smth with the htmlBody
}

fullLoad('your/url/', callback);
Manu
источник
3

Это реализация ответа Супра. Также он использует setTimeout вместо setInterval, как предложил Матеуш Чаритонюк.

Phantomjs выйдет через 1000 мсек, когда не будет никакого запроса или ответа.

// load the module
var webpage = require('webpage');
// get timestamp
function getTimestamp(){
    // or use Date.now()
    return new Date().getTime();
}

var lastTimestamp = getTimestamp();

var page = webpage.create();
page.onResourceRequested = function(request) {
    // update the timestamp when there is a request
    lastTimestamp = getTimestamp();
};
page.onResourceReceived = function(response) {
    // update the timestamp when there is a response
    lastTimestamp = getTimestamp();
};

page.open(html, function(status) {
    if (status !== 'success') {
        // exit if it fails to load the page
        phantom.exit(1);
    }
    else{
        // do something here
    }
});

function checkReadyState() {
    setTimeout(function () {
        var curentTimestamp = getTimestamp();
        if(curentTimestamp-lastTimestamp>1000){
            // exit if there isn't request or response in 1000ms
            phantom.exit();
        }
        else{
            checkReadyState();
        }
    }, 100);
}

checkReadyState();
Dayong
источник
3

Этот код я использую:

var system = require('system');
var page = require('webpage').create();

page.open('http://....', function(){
      console.log(page.content);
      var k = 0;

      var loop = setInterval(function(){
          var qrcode = page.evaluate(function(s) {
             return document.querySelector(s).src;
          }, '.qrcode img');

          k++;
          if (qrcode){
             console.log('dataURI:', qrcode);
             clearInterval(loop);
             phantom.exit();
          }

          if (k === 50) phantom.exit(); // 10 sec timeout
      }, 200);
  });

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

Рокко Мусолино
источник
3

Я использую личную смесь waitfor.jsпримера фантома .

Это мой main.jsфайл:

'use strict';

var wasSuccessful = phantom.injectJs('./lib/waitFor.js');
var page = require('webpage').create();

page.open('http://foo.com', function(status) {
  if (status === 'success') {
    page.includeJs('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js', function() {
      waitFor(function() {
        return page.evaluate(function() {
          if ('complete' === document.readyState) {
            return true;
          }

          return false;
        });
      }, function() {
        var fooText = page.evaluate(function() {
          return $('#foo').text();
        });

        phantom.exit();
      });
    });
  } else {
    console.log('error');
    phantom.exit(1);
  }
});

И lib/waitFor.jsфайл (который является просто копией и waifFor()вставкой функции из waitfor.jsпримера phantomjs ):

function waitFor(testFx, onReady, timeOutMillis) {
    var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3000, //< Default Max Timout is 3s
        start = new Date().getTime(),
        condition = false,
        interval = setInterval(function() {
            if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                // If not time-out yet and condition not yet fulfilled
                condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
            } else {
                if(!condition) {
                    // If condition still not fulfilled (timeout but condition is 'false')
                    console.log("'waitFor()' timeout");
                    phantom.exit(1);
                } else {
                    // Condition fulfilled (timeout and/or condition is 'true')
                    // console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                    typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condi>
                    clearInterval(interval); //< Stop this interval
                }
            }
        }, 250); //< repeat check every 250ms
}

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

Дайси
источник
2

Это старый вопрос, но так как я искал полную загрузку страницы, но для Spookyjs (которая использует casperjs и phantomjs) и не нашел своего решения, я сделал для этого свой собственный скрипт, с тем же подходом, что и пользователь deemstone. Этот подход в течение определенного периода времени, если страница не получила или не запустила какой-либо запрос, завершит выполнение.

В файле casper.js (если вы установили его глобально, путь будет выглядеть примерно так: /usr/local/lib/node_modules/casperjs/modules/casper.js) добавьте следующие строки:

В верхней части файла со всеми глобальными переменными:

var waitResponseInterval = 500
var reqResInterval = null
var reqResFinished = false
var resetTimeout = function() {}

Затем внутри функции "createPage (casper)" сразу после "var page = require ('webpage'). Create ();" добавьте следующий код:

 resetTimeout = function() {
     if(reqResInterval)
         clearTimeout(reqResInterval)

     reqResInterval = setTimeout(function(){
         reqResFinished = true
         page.onLoadFinished("success")
     },waitResponseInterval)
 }
 resetTimeout()

Затем внутри "page.onResourceReceived = function onResourceReceived (resource) {" в первой строке добавьте:

 resetTimeout()

Сделайте то же самое для "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Наконец, на странице «page.onLoadFinished = function onLoadFinished (status) {» в первой строке добавьте:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

И это все, надеюсь, что это поможет кому-то в беде, как я. Это решение для casperjs, но работает напрямую для Spooky.

Удачи !

fdnieves
источник
0

это мое решение, это сработало для меня.

page.onConsoleMessage = function(msg, lineNum, sourceId) {

    if(msg=='hey lets take screenshot')
    {
        window.setInterval(function(){      
            try
            {               
                 var sta= page.evaluateJavaScript("function(){ return jQuery.active;}");                     
                 if(sta == 0)
                 {      
                    window.setTimeout(function(){
                        page.render('test.png');
                        clearInterval();
                        phantom.exit();
                    },1000);
                 }
            }
            catch(error)
            {
                console.log(error);
                phantom.exit(1);
            }
       },1000);
    }       
};


page.open(address, function (status) {      
    if (status !== "success") {
        console.log('Unable to load url');
        phantom.exit();
    } else { 
       page.setContent(page.content.replace('</body>','<script>window.onload = function(){console.log(\'hey lets take screenshot\');}</script></body>'), address);
    }
});
Том
источник