phantomjs не чекає "повного" завантаження сторінки


137

Я використовую PhantomJS v1.4.1 для завантаження деяких веб-сторінок. У мене немає доступу до їх сервера, я просто отримую посилання на них. Я використовую застарілу версію Phantom, оскільки мені потрібно підтримувати Adobe Flash на цих веб-сторінках.

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


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

1
Так, наразі я дотримувався такого підходу.
nilfalse

102
Це жахливе рішення, вибачте (це вина PhantomJS!). Якщо ви зачекаєте повну секунду, але завантаження займає 20 мс, це повна марна трата часу (думаю, що пакетні завдання), або якщо це займе більше секунди, воно все одно не вийде. Така неефективність та ненадійність є нестерпним для професійної роботи.
CodeManX

9
Справжня проблема тут полягає в тому, що ви ніколи не знаєте, коли JavaScript закінчить завантажувати сторінку, а браузер також не знає цього. Уявіть собі сайт, на якому якийсь javascript завантажує щось із сервера у нескінченний цикл. З точки зору браузера - виконання JavaScript ніколи не закінчується, тож у який момент ви хочете, щоб Phantomjs сказав вам, що він закінчився? Ця проблема не вирішується в загальних випадках, за винятком випадків очікування рішення, що очікує на очікування, та сподівання на найкраще.
Максим Галушка

5
Це все-таки найкраще рішення станом на 2016 рік? Здається, нам слід зробити краще, ніж це.
Адам Томпсон

6
Якщо ви керуєте кодом, який ви намагаєтеся прочитати, ви можете зателефонувати фантомному js передзвонити явно: 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має затримку за замовчуванням 4ms ( https://stackoverflow.com/a/3580085/1011156 ), тому активне опитування не вплине різко на продуктивність програми.

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


4
коментар на setTimeout vs setInterval чудовий.
Гал Брача

1
readyStateзапуститься лише після того, як DOM буде повністю завантажений, проте будь-які <iframe>елементи все ще можуть завантажуватися, тому він насправді не відповідає на початковий запитання
CodingIntrigue

1
@rgraham Це не ідеально, але я думаю, що ми можемо зробити стільки з цими рендерами. Будуть крайові випадки, коли ви просто не дізнаєтесь, чи щось завантажено повністю. Подумайте про сторінку, де вміст затягується спеціально на хвилину-дві. Нерозумно очікувати, що процес візуалізації буде сидіти і чекати невизначений проміжок часу. Те саме стосується вмісту, завантаженого із зовнішніх джерел, який може бути повільним.
Брендон Елліотт

3
Це не враховує будь-яке завантаження JavaScript після повного завантаження DOM, наприклад, за допомогою магістралі / вугілля / кута
Адам Томпсон

1
Зовсім не працював для мене. ReadyState завершено, можливо, запустився, але сторінка в цьому місці була порожньою.
Стів Стейпл

21

Ви можете спробувати комбінацію прикладів очікування та растрових прикладів:

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

3
Схоже, це не буде працювати з веб-сторінками, які використовують будь-які технології серверних натискань, оскільки ресурс все ще буде використовуватися після появи 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робочий приклад.


Але в цьому випадку я не можу використовувати один екземпляр PhantomJS для завантаження більше однієї сторінки одночасно, правда?
nilfalse

Чи застосовується onResourceRequested до запитів AJAX / Cross Domain? Або це стосується лише таких як css, images .. тощо?
CMCDragonkai

@CMCDragonkai я ніколи не використовував його сам, але виходячи з цього, схоже, він включає всі запити. Цитата:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr

Я використовував цей метод з широкомасштабним відображенням PhantomJS, і він працює досить добре. Вам потрібно багато розумних для відстеження запитів і перегляду, якщо вони не спрацьовують або закінчуються. Більше інформації: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty

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

Ви повинні перевірити, що відповідь.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;
        }
    }
}

11

Я вважаю такий підхід корисним у деяких випадках:

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

Тоді якщо вам належить сторінка, покладіть всередину якийсь сценарій:

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

Це виглядає як дуже приємна робота, однак я не зміг отримати жодне повідомлення з моєї сторінки HTML / JavaScript, щоб пройти через phantomJS ... подія onConsoleMessage ніколи не спрацьовувало, коли я міг чудово бачити повідомлення на консолі браузера, і У мене немає поняття, чому.
Дірк

1
Мені потрібна page.onConsoleMessage = функція (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);

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

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

Я використовую персональну суміш прикладу phantomjswaitfor.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) і не знайшов свого рішення, я створив для цього свій власний сценарій, з таким же підходом, що і користувач вважає. Цей підхід робить за певний проміжок часу, якщо сторінка не отримала або не розпочала жодного запиту, вона закінчить виконання.

У файл 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 = requ ('webpage'). Create ();" додати наступний код:

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

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

Потім всередину "page.onResourceReceived = функція onResourceReceived (ресурс) {" у першому рядку додайте:

 resetTimeout()

Зробіть те ж саме для "page.onResourceRequested = функція onResourceRequested (requestData, request) {"

Нарешті, на "page.onLoadFinished = функція onLoadFinished (статус) {" у першому рядку додайте:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

І це все, сподіваюся, що цей допомагає комусь у біді, як я. Це рішення призначене для casperjs, але працює безпосередньо для Spooky.

Удачі !


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);
    }
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.