phantomjs n'attend pas le chargement" complet " de la page

j'utilise PhantomJS v1.4.1 pour charger quelques pages web. Je n'ai pas accès à leur côté serveur, je reçois juste des liens pointant vers eux. J'utilise la version obsolète de Phantom parce que j'ai besoin de supporter Adobe Flash sur ces pages web.

le problème est que de nombreux sites Web chargent leur contenu mineur async et c'est pourquoi le callback onLoadFinished de Phantom (analogique pour onLoad en HTML) se déclenche trop tôt alors que tout n'est pas encore chargé. Est-ce que quelqu'un peut suggérer comment je peux attendre la pleine charge d'une page Web pour faire, par exemple, une capture d'écran avec tout le contenu dynamique comme les annonces?

128
demandé sur Cybermaxs 2012-07-05 11:53:19

13 réponses

une autre approche consiste à simplement demander à PhantomJS d'attendre un peu après le chargement de la page avant de faire le rendu, comme dans la rastérize habituelle .js exemple, mais avec un délai plus long pour permettre au JavaScript de finir de charger des ressources supplémentaires:

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 
    }
});
69
répondu rhunwicks 2013-02-07 10:36:54

je préférerais vérifier périodiquement le statut document.readyState ( ) https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Bien que cette approche soit un peu compliquée, vous pouvez être sûr qu'à l'intérieur de la fonction onPageReady vous utilisez un document complètement chargé.

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

explication supplémentaire:

utilisant imbriqué setTimeout au lieu de setInterval empêche checkReadyState des conditions de "chevauchement" et de race lorsque son exécution est prolongée pour des raisons aléatoires. setTimeout a un délai par défaut de 4ms ( https://stackoverflow.com/a/3580085/1011156 ) ainsi le sondage actif n'affectera pas de façon drastique la performance du programme.

document.readyState === "complete" signifie que le document est complètement chargé avec toutes les ressources ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

49
répondu Mateusz Charytoniuk 2017-05-23 12:26:35

vous pouvez essayer une combinaison des exemples waitfor et 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);
        }
    });
}
20
répondu rhunwicks 2013-02-07 10:32:31

peut-être Pouvez-vous utiliser les onResourceRequested et onResourceReceived " callbacks pour détecter la charge asynchrone. Voici un exemple d'utilisation des callbacks de leur documentation :

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

Aussi, vous pouvez regarder examples/netsniff.js pour un exemple.

13
répondu Supr 2012-07-05 08:04:05

dans mon programme, j'utilise une certaine logique pour juger si c'était onload: regarder sa requête réseau, s'il n'y avait pas de nouvelle requête sur plus de 200ms, je la traitais onload.

utilisez ceci, après 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;
        }
    }
}
13
répondu deemstone 2014-12-14 20:12:36

Voici une solution qui attend que toutes les demandes de ressources soient complétées. Une fois terminé, il enregistrera le contenu de la page sur la console et générera une capture d'écran de la page rendue.

bien que cette solution puisse servir de bon point de départ, je l'ai vu échouer donc ce n'est certainement pas une solution complète!

Je n'ai pas eu beaucoup de chance avec document.readyState .

j'ai été influencé par le waitfor.js exemple trouvé sur la phantomjs exemples page .

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);
});
13
répondu Dave 2016-08-25 17:00:40

j'ai trouvé cette approche utile dans certains cas:

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

que si vous possédez la page mettre un script à l'intérieur:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
11
répondu Brankodd 2015-06-12 14:23:03

j'ai trouvé cette solution utile dans une application NodeJS. Je l'utilise juste dans les cas désespérés parce qu'il lance un délai d'attente afin d'attendre le chargement complet de la page.

le second argument est la fonction callback qui va être appelée une fois que la réponse est prête.

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
répondu Manu 2015-06-18 14:44:10

il s'agit d'une mise en œuvre de la réponse de Supr. Il utilise aussi setTimeout au lieu de setInterval comme Mateusz Charytoniuk l'a suggéré.

Phantomjs sortira en 1000ms quand il n'y a pas de demande ou de réponse.

// 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
répondu Dayong 2016-06-30 20:11:38

C'est le code que j'utilise:

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

essentiellement étant donné le fait que vous êtes censé savoir que la page est entièrement téléchargée quand un élément donné apparaît sur le DOM. Donc le script va attendre jusqu'à ce que ça arrive.

3
répondu Rocco Musolino 2016-07-01 15:44:44

c'est une vieille question, mais comme je cherchais une pleine page mais pour Spookyjs (qui utilise casperjs et phantomjs) et n'ai pas trouvé ma solution, j'ai fait mon propre script pour cela, avec la même approche que l'utilisateur deemstone . Ce que fait cette approche, c'est que, pour une période de temps donnée, si la page n'a pas reçu ou démarré de requête, elle mettra fin à l'exécution.

sur casper.fichier js (si vous l'avez installé globalement, le chemin serait quelque chose comme /usr/local/lib/node_modules/casperjs/modules / casper.js) ajouter les lignes suivantes:

en haut du fichier avec toutes les variables globales:

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

Puis à l'intérieur de la fonction "createPage(casper)" juste après "var page = require('page').créer (); "ajouter le code suivant:

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

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

Puis à l'intérieur de la page".onResourceReceived = fonction onResourceReceived(ressource) {" sur la première ligne ajouter:

 resetTimeout()

Faire de même ".onressourceequested = function onressourceequested (requestData, request) {"

Enfin, sur la page".onLoadFinished = fonction onLoadFinished(statut) {" sur la première ligne ajouter:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Et voilà, espérons que cela aide quelqu'un en difficulté, comme je l'étais. Cette solution est pour casperjs mais fonctionne directement pour Spooky.

bonne chance !

2
répondu fdnieves 2016-01-28 15:40:27

j'utilise un mélange personnel des fantômes waitfor.js exemple .

C'est mon fichier 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);
  }
});

et le fichier lib/waitFor.js (qui est juste une copie et une pâte de la fonction waifFor() de la phantomjs waitfor.js exemple ):

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
}

cette méthode n'est pas asynchrone mais au moins suis-je assuré que toutes les ressources ont été chargé avant j'essaie de les utiliser.

2
répondu Daishi 2016-10-12 14:40:59

c'est ma solution qui a fonctionné pour moi .

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);
    }
});
0
répondu Tom 2017-07-21 12:04:50