web-dev-qa-db-fra.com

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

J'utilise PhantomJS v1.4.1 pour charger des pages Web. Je n'ai pas accès à leur côté serveur, je reçois juste des liens pointant vers eux. J'utilise une version obsolète de Phantom car je dois prendre en charge Adobe Flash sur ces pages Web.

Le problème est que beaucoup de sites Web chargent leur contenu mineur asynchrone et c'est pourquoi le rappel onLoadFinished de Phantom (analogique pour onLoad en HTML) a été déclenché trop tôt alors que tout n'était pas encore chargé. Quelqu'un peut-il suggérer comment puis-je attendre le chargement complet d'une page Web pour créer, par exemple, une capture d'écran avec tout le contenu dynamique comme les annonces?

136
nilfalse

Une autre approche consiste simplement à demander à PhantomJS d'attendre un peu après le chargement de la page avant d'effectuer le rendu, comme dans l'exemple habituel rasterize.js , mais avec un délai plus long pour permettre le chargement du JavaScript. ressources additionnelles:

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

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 maladroite, vous pouvez être sûr que dans la fonction onPageReady, vous utilisez un document entièrement 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:

L'utilisation de setTimeout imbriqué au lieu de setInterval empêche checkReadyState de "chevaucher" et les conditions de concurrence lorsque son exécution est prolongée pour des raisons aléatoires. setTimeout a un délai par défaut de 4 ms ( https://stackoverflow.com/a/3580085/1011156 ) de sorte qu'une interrogation active n'affectera pas considérablement les performances 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 ).

51
Mateusz Charytoniuk

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
rhunwicks

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

Bien que cette solution puisse constituer un bon point de départ, j’ai constaté qu’elle échouait et qu’elle n’est donc certainement pas une solution complète!

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

J'ai été influencé par l'exemple waitfor.js trouvé sur la page page d'exemples 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);
});
14
Dave

Dans mon programme, j’utilise une certaine logique pour déterminer s’il était en surcharge: en regardant sa requête réseau, s’il n’y avait pas de nouvelle requête au-delà de 200 ms, je la traitais en surcharge.

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
deemstone

Peut-être pouvez-vous utiliser les rappels onResourceRequested ET onResourceReceived pour détecter le chargement asynchrone. Voici un exemple d'utilisation de ces rappels 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);

Vous pouvez également consulter examples/netsniff.js pour un exemple de travail.

13
Supr

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

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

Si vous possédez la page, mettez un script à l'intérieur:

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

Ceci est une mise en œuvre de la réponse de Supr. En outre, il utilise setTimeout au lieu de setInterval comme suggéré par Mateusz Charytoniuk.

Phantomjs se terminera dans 1 000 ms lorsqu'il n'y aura ni demande ni 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
Dayong

J'ai trouvé cette solution utile dans une application NodeJS. Je ne l'utilise que dans des cas désespérés car il lance un délai d'attente afin d'attendre le chargement complet de la page.

Le deuxième argument est la fonction de rappel qui sera appelée une fois la réponse 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
Manu

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

Fondamentalement, étant donné que vous êtes censé savoir que la page est entièrement téléchargée lorsqu'un élément donné apparaît sur le DOM. Donc, le script va attendre que cela se produise.

3
Rocco Musolino

C'est une vieille question, mais comme je cherchais un chargement complet de la page mais pas Spookyjs (qui utilise casperjs et phantomjs) et que je ne trouvais pas ma solution, j'ai créé mon propre script à cet effet, avec la même approche que celle utilisée par l'utilisateur. Ce que fait cette approche est, pour un laps de temps donné, si la page n'a pas reçu ou commencé une demande, elle mettra fin à l'exécution.

Sur le fichier casper.js (si vous l'avez installé globalement, le chemin serait du type /usr/local/lib/node_modules/casperjs/modules/casper.js), ajoutez les lignes suivantes:

En haut du fichier avec tous les vars globaux:

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

Ensuite, dans la fonction "createPage (casper)" juste après "var page = require ('page Web'). Create ();" ajoutez le code suivant:

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

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

Ensuite, dans "page.onResourceReceived = fonction onResourceReceived (resource) {" sur la première ligne, ajoutez:

 resetTimeout()

Faites de même pour "page.onResourceRequested = fonction onResourceRequested (requestData, request) {"

Enfin, sur "page.onLoadFinished = function onLoadFinished (status) {" sur la première ligne, ajoutez:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Et ça y est, espérons que celui-ci aide quelqu'un qui a des problèmes comme moi. Cette solution est pour casperjs mais fonctionne directement pour Spooky.

Bonne chance !

2
fdnieves

J'utilise un mélange personnel de phantomjs waitfor.js example .

Voici 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 simplement un copier-coller de la fonction waifFor() du fantôme 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, j’ai la certitude que toutes les ressources ont été chargées avant d’essayer de les utiliser.

2
Daishi

c'est ma solution sa a travaillé 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
Tom