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

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

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

13 ответов

Другой подход - просто попросить 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 
    }
});

Я бы предпочел периодически проверять 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 мс ( /questions/3984234/yavlyaetsya-li-settimeout-bez-zaderzhki-takim-zhe-kak-mgnovennoe-vyipolnenie-funktsii/3984244#3984244), поэтому активный опрос не окажет существенного влияния на производительность программы.

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

Вы можете попробовать комбинацию примеров 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);
        }
    });
}

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

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

Мне не очень повезло, используя 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);
});

Может быть, вы можете использовать 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 для рабочего примера.

В моей программе я использую некоторую логику, чтобы судить, была ли она загружена: при просмотре сетевого запроса, если не было нового запроса за последние 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;
        }
    }
}

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

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

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

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>

Я нашел это решение полезным в приложении 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);

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

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. Так что сценарий будет ждать, пока это не произойдет.

Это реализация ответа Супра. Также он использует 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();

Я использую личную смесь фантомов 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 пример):

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
}

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

Это старый вопрос, но так как я искал полную загрузку страницы, но для 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.

Удачи!

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

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);
    }
});

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

 page.sendEvent('click',200, 660);

do { phantom.page.sendEvent('mousemove'); } while (page.loading);

ОБНОВИТЬ

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

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

page.evaluate(function () {
document.getElementsByClassName('btn btn-primary btn-block')[0].click();
});
do { phantom.page.sendEvent('mousemove'); } while (page.evaluate(function()
{
return document.location != "https://www.bestwaywholesale.co.uk/";
}));
do { phantom.page.sendEvent('mousemove'); } while (page.loading);
Другие вопросы по тегам