phantomjs n'attend pas le chargement de la page «complète»


137

J'utilise PhantomJS v1.4.1 pour charger certaines 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 de nombreux sites Web chargent leur contenu mineur de manière asynchrone et c'est pourquoi le rappel onLoadFinished de Phantom (analogique pour onLoad en HTML) est déclenché trop tôt alors que tout n'est pas encore chargé. Quelqu'un peut-il suggérer comment puis-je attendre le chargement complet d'une page Web pour faire, par exemple, une capture d'écran avec tout le contenu dynamique comme les publicités?


3
Je pense qu'il est temps d'accepter une réponse
spartikus

Réponses:


76

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 l' exemple habituel de rasterize.js , mais avec un délai d'expiration plus long pour permettre au JavaScript de terminer le chargement de 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 
    }
});

1
Oui, actuellement je suis resté fidèle à cette approche.
nilfalse

102
C'est une solution horrible, désolé (c'est la faute de PhantomJS!). Si vous attendez une seconde complète, mais que le chargement prend 20 ms, c'est une perte de temps totale (pensez aux travaux par lots), ou si cela prend plus d'une seconde, cela échouera toujours. Une telle inefficacité et manque de fiabilité sont insupportables pour le travail professionnel.
CodeManX

9
Le vrai problème ici est que vous ne savez jamais quand javascript finira de charger la page et que le navigateur ne le sait pas non plus. Imaginez un site qui a du javascript qui charge quelque chose du serveur en boucle infinie. Du point de vue du navigateur - l'exécution de javascript ne se termine jamais, alors quel est le moment où vous voulez que phantomjs vous dise qu'elle est terminée? Ce problème est insoluble dans le cas générique sauf avec une solution d'attente pour le délai d'expiration et l'espoir pour le meilleur.
Maxim Galushka

5
Est-ce toujours la meilleure solution à partir de 2016? Il semble que nous devrions pouvoir faire mieux que cela.
Adam Thompson

6
Si vous contrôlez le code que vous essayez de lire, vous pouvez appeler le call back phantom js explicitement: phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith

52

Je préfère vérifier périodiquement le document.readyStatestatut ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Bien que cette approche soit un peu maladroite, vous pouvez être sûr qu'à l'intérieur de la onPageReadyfonction 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:

Utiliser nested setTimeoutau lieu de setIntervalempêche le checkReadyState"chevauchement" et les conditions de concurrence lorsque son exécution est prolongée pour des raisons aléatoires. setTimeouta un délai par défaut de 4 ms ( https://stackoverflow.com/a/3580085/1011156 ) de sorte que l'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 ).


4
le commentaire sur setTimeout vs setInterval est super.
Gal Bracha

1
readyStatene se déclenchera qu'une fois le DOM complètement chargé, mais tous les <iframe>éléments peuvent encore être en cours de chargement, donc cela ne répond pas vraiment à la question d'origine
CodingIntrigue

1
@rgraham Ce n'est pas l'idéal mais je pense que nous ne pouvons pas faire grand-chose avec ces moteurs de rendu. Il y aura des cas extrêmes où vous ne saurez tout simplement pas si quelque chose est complètement chargé. Pensez à une page dont le contenu est retardé, exprès, d'une minute ou deux. Il est déraisonnable de s'attendre à ce que le processus de rendu reste en suspens et attende un laps de temps indéfini. Il en va de même pour le contenu chargé à partir de sources externes qui peut être lent.
Brandon Elliott

3
Cela ne prend pas en compte le chargement de JavaScript après le chargement complet du DOM, comme avec Backbone / Ember / Angular.
Adam Thompson

1
Cela n'a pas du tout fonctionné pour moi. readyState complete a peut-être été déclenché, mais la page était vide à ce stade.
Steve Staple

21

Vous pouvez essayer une combinaison des exemples d'attente et de pixellisation:

/**
 * 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
On dirait que cela ne fonctionnerait pas avec les pages Web, qui utilisent l'une des technologies de poussée de serveur, car la ressource sera toujours utilisée après onLoad.
nilfalse

Faites des pilotes, par exemple. poltergeist , avez une fonctionnalité comme celle-ci?
Jared Beck

Est-il possible d'utiliser waitFor pour interroger tout le texte html et rechercher un mot-clé défini? J'ai essayé de mettre en œuvre cela mais il semble que le sondage ne se rafraîchisse pas à la dernière source html téléchargée.
fpdragon

14

Vous pouvez peut-être utiliser les rappels onResourceRequestedetonResourceReceived 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.jsun exemple de travail.


Mais dans ce cas, je ne peux pas utiliser une seule instance de PhantomJS pour charger plus d'une page à la fois, non?
nilfalse

OnResourceRequested s'applique-t-il aux demandes AJAX / Cross Domain? Ou cela s'applique-t-il uniquement aux css, aux images, etc.?
CMCDragonkai

@CMCDragonkai Je ne l'ai jamais utilisé moi-même, mais sur cette base, il semble qu'il inclut toutes les demandes. Citation:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr

J'ai utilisé cette méthode avec un rendu PhantomJS à grande échelle et cela fonctionne assez bien. Vous avez besoin de beaucoup d'intelligence pour suivre les demandes et voir si elles échouent ou expirent. Plus d'infos: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty

14

Voici une solution qui attend que toutes les demandes de ressources se terminent. 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, j'ai constaté qu'elle échouait donc ce n'est certainement pas une solution complète!

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

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

A donné un coup de pouce, mais a utilisé setTimeout avec 10, au lieu d'intervalle
GDmac

Vous devez vérifier que response.stage est égal à 'end' avant de le supprimer du tableau requests, sinon il pourrait être supprimé prématurément.
Reimund

Cela ne fonctionne pas si votre page Web charge le DOM dynamiquement
Buddy

13

Dans mon programme, j'utilise une logique pour juger s'il était en charge: en regardant sa requête réseau, s'il n'y avait pas de nouvelle requête sur les 200 ms passés, je la traite en charge.

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

11

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, mettez un script à l'intérieur:

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

Cela ressemble à une solution de contournement vraiment sympa, cependant, je n'ai pas pu obtenir de message de journal de ma page HTML / JavaScript pour passer par phantomJS ... l'événement onConsoleMessage ne s'est jamais déclenché alors que je pouvais voir parfaitement les messages sur la console du navigateur, et Je n'ai aucune idée pourquoi.
Dirk

1
J'avais besoin de page.onConsoleMessage = function (msg) {};
Andy Balaam

5

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

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

Ceci est une implémentation de la réponse de Supr. Il utilise également setTimeout au lieu de setInterval comme le suggérait Mateusz Charytoniuk.

Phantomjs se fermera dans 1000 ms lorsqu'il n'y aura aucune demande ou 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

Voici 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. Le script va donc attendre que cela se produise.


3

J'utilise un mélange personnel de l' waitfor.jsexemple phantomjs .

Voici mon main.jsdossier:

'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 lib/waitFor.jsfichier (qui est juste un copier-coller de la waifFor()fonction de l' waitfor.jsexemple 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
}

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


2

C'est une vieille question, mais comme je cherchais un chargement complet de la page mais pour Spookyjs (qui utilise casperjs et phantomjs) et que je n'ai pas trouvé ma solution, j'ai créé mon propre script pour cela, avec la même approche que l'utilisateur estime. Ce que fait cette approche, c'est que, pendant une durée donnée, 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 quelque chose comme /usr/local/lib/node_modules/casperjs/modules/casper.js) ajoutez 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 ('webpage'). Create ();" ajoutez le code suivant:

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

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

Puis à l'intérieur de "page.onResourceReceived = function onResourceReceived (resource) {" sur la première ligne, ajoutez:

 resetTimeout()

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

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

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Et c'est tout, j'espère que celui-ci aidera quelqu'un en difficulté comme moi. Cette solution est pour casperjs mais fonctionne directement pour Spooky.

Bonne chance !


0

c'est ma solution son 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);
    }
});
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.