phantomjs non attende il caricamento della pagina "completo"


137

Sto usando PhantomJS v1.4.1 per caricare alcune pagine Web. Non ho accesso al loro lato server, sto solo ricevendo collegamenti che puntano a loro. Sto usando una versione obsoleta di Phantom perché ho bisogno di supportare Adobe Flash su quelle pagine web.

Il problema è che molti siti web stanno caricando il loro contenuto secondario asincrono ed è per questo che il callback onLoadFinished di Phantom (analogo per onLoad in HTML) è stato attivato troppo presto quando non tutto è ancora caricato. Qualcuno può suggerire come posso attendere il pieno caricamento di una pagina Web per creare, ad esempio, uno screenshot con tutti i contenuti dinamici come gli annunci?


3
Penso che sia tempo di accettare una risposta
spartikus,

Risposte:


76

Un altro approccio è semplicemente chiedere a PhantomJS di attendere un po 'dopo che la pagina è stata caricata prima di eseguire il rendering, come nel normale esempio di rasterize.js , ma con un timeout più lungo per consentire a JavaScript di completare il caricamento di risorse aggiuntive:

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
Sì, attualmente mi sono bloccato su questo approccio.
nilfalse,

102
È una soluzione orribile, scusa (è colpa di PhantomJS!). Se aspetti un secondo intero, ma ci vogliono 20 ms per caricarlo, è una perdita di tempo completa (pensa a lavori batch) o se impiega più di un secondo, continuerà comunque a fallire. Tale inefficienza e inaffidabilità sono insopportabili per il lavoro professionale.
CodeManX,

9
Il vero problema qui è che non sai mai quando javascript finirà di caricare la pagina e il browser non lo sa. Immagina un sito con javascript che carica qualcosa dal server in un ciclo infinito. Dal punto di vista del browser, l'esecuzione di javascript non finisce mai, quindi qual è quel momento in cui vuoi che phantomjs ti dica che è finito? Questo problema è irrisolvibile in casi generici, tranne con l'attesa della soluzione di timeout e la speranza per il meglio.
Maxim Galushka,

5
È ancora la soluzione migliore a partire dal 2016? Sembra che dovremmo essere in grado di fare meglio di così.
Adam Thompson,

6
Se hai il controllo del codice che stai cercando di leggere, puoi chiamare il fantasma js richiamando esplicitamente: phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith

52

Preferirei periodicamente verificare lo document.readyStatestato ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Sebbene questo approccio sia un po 'goffo, puoi essere sicuro che all'interno della onPageReadyfunzione stai utilizzando un documento completamente caricato.

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

Spiegazione aggiuntiva:

L'uso di nidificati setTimeoutanziché setIntervalimpedisce la checkReadyState"sovrapposizione" e le condizioni di competizione quando la sua esecuzione è prolungata per alcuni motivi casuali. setTimeoutha un ritardo predefinito di 4 ms ( https://stackoverflow.com/a/3580085/1011156 ), pertanto il polling attivo non influirà drasticamente sulle prestazioni del programma.

document.readyState === "complete"significa che il documento è completamente caricato con tutte le risorse ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).


4
il commento su setTimeout vs setInterval è fantastico.
Gal Bracha,

1
readyStatesi attiverà solo dopo che il DOM sarà stato caricato completamente, tuttavia è <iframe>possibile che vengano ancora caricati elementi in modo che non risponda realmente alla domanda originale
CodingIntrigue

1
@rgraham Non è l'ideale ma penso che possiamo fare così tanto con questi renderer. Ci saranno casi limite in cui non saprai se qualcosa è stato caricato completamente. Pensa a una pagina in cui il contenuto è ritardato, di proposito, di un minuto o due. È irragionevole aspettarsi che il processo di rendering rimanga fermo e attendere un periodo di tempo indefinito. Lo stesso vale per i contenuti caricati da fonti esterne che potrebbero essere lenti.
Brandon Elliott,

3
Questo non considera alcun caricamento JavaScript dopo il caricamento completo di DOM, come ad esempio Backbone / Ember / Angular.
Adam Thompson,

1
Non ha funzionato affatto per me. readyState complete potrebbe essere stato attivato, ma la pagina era vuota a questo punto.
Steve Staple,

21

Puoi provare una combinazione degli esempi waitfor e 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);
        }
    });
}

3
Sembra che non funzionerebbe con le pagine Web, che utilizzano una qualsiasi delle tecnologie push del server, poiché la risorsa sarà ancora in uso dopo che si è verificato onLoad.
nilfalse,

Fare qualsiasi driver, ad es. poltergeist , hai una caratteristica come questa?
Jared Beck,

È possibile utilizzare waitFor per eseguire il polling dell'intero testo html e cercare una parola chiave definita? Ho provato a implementarlo, ma sembra che il polling non si aggiorni all'ultima fonte html scaricata.
fpdragon,

14

Forse è possibile utilizzare i callback onResourceRequestedeonResourceReceived per rilevare il caricamento asincrono. Ecco un esempio di utilizzo di quei callback dalla loro documentazione :

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

Inoltre, puoi cercare examples/netsniff.jsun esempio funzionante.


Ma in questo caso non posso usare un'istanza di PhantomJS per caricare più di una pagina alla volta, giusto?
nilfalse,

OnResourceRequested si applica alle richieste AJAX / Cross Domain? O si applica solo a come CSS, immagini ... ecc?
CMCDragonkai,

@CMCDragonkai Non l'ho mai usato da solo, ma in base a questo sembra che includa tutte le richieste. Citazione:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr

Ho usato questo metodo con rendering PhantomJS su larga scala e funziona abbastanza bene. Hai bisogno di molta intelligenza per tenere traccia delle richieste e controllare se falliscono o scadono. Maggiori informazioni: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty

14

Ecco una soluzione che attende il completamento di tutte le richieste di risorse. Una volta completato, registrerà il contenuto della pagina sulla console e genererà uno screenshot della pagina renderizzata.

Sebbene questa soluzione possa servire come un buon punto di partenza, ho osservato che fallisce, quindi non è sicuramente una soluzione completa!

Non ho avuto molta fortuna usando document.readyState.

Sono stato influenzato dall'esempio waitfor.js trovato nella pagina degli esempi di 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);
});

Ho dato un pollice in su, ma ho usato setTimeout con 10, invece di intervallo
GDmac,

È necessario verificare che response.stage sia uguale a "end" prima di rimuoverlo dall'array delle richieste, altrimenti potrebbe essere rimosso prematuramente.
Reimund,

Questo non funziona se la tua pagina web carica il DOM in modo dinamico
Buddy,

13

Nel mio programma, utilizzo una logica per giudicare se è stato caricato: osservando la sua richiesta di rete, se non ci sono state nuove richieste negli ultimi 200 ms, lo tratto onload.

Usa questo, dopo 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

Ho trovato questo approccio utile in alcuni casi:

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

Che se possiedi la pagina inserisci alcuni script all'interno:

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

Sembra una soluzione davvero interessante, tuttavia, non sono riuscito a ricevere alcun messaggio di log dalla mia pagina HTML / JavaScript per passare attraverso phantomJS ... l'evento onConsoleMessage non si è mai attivato mentre vedevo perfettamente i messaggi sulla console del browser e Non ho idea del perché.
Dirk,

1
Avevo bisogno di page.onConsoleMessage = function (msg) {};
Andy Balaam,

5

Ho trovato questa soluzione utile in un'app NodeJS. Lo uso solo in casi disperati perché avvia un timeout per attendere il caricamento dell'intera pagina.

Il secondo argomento è la funzione di callback che verrà chiamata una volta pronta la risposta.

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

Questa è un'implementazione della risposta di Supr. Inoltre usa setTimeout invece di setInterval come suggerito da Mateusz Charytoniuk.

Phantomjs uscirà tra 1000 ms quando non c'è alcuna richiesta o risposta.

// 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

Questo è il codice che uso:

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

Fondamentalmente dato il fatto che dovresti sapere che la pagina è completamente scaricata quando un determinato elemento appare sul DOM. Quindi lo script aspetterà che ciò accada.


3

Io uso una miscela personnal del phantomjs waitfor.jsesempio .

Questo è il mio main.jsfile:

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

E il lib/waitFor.jsfile (che è solo un copia ed incolla della waifFor()funzione dal phantomjs waitfor.jsesempio ):

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
}

Questo metodo non è asincrono, ma almeno sono sicuro che tutte le risorse sono state caricate prima di provare a usarle.


2

Questa è una vecchia domanda, ma dato che stavo cercando il caricamento della pagina intera ma per Spookyjs (che utilizza casperjs e phantomjs) e non ho trovato la mia soluzione, ho creato il mio script per quello, con lo stesso approccio dell'utente deemstone. Ciò che fa questo approccio è, per un dato periodo di tempo, se la pagina non ha ricevuto o avviato alcuna richiesta, termina l'esecuzione.

Nel file casper.js (se lo hai installato a livello globale, il percorso sarebbe simile a /usr/local/lib/node_modules/casperjs/modules/casper.js) aggiungi le seguenti righe:

Nella parte superiore del file con tutti i var globali:

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

Quindi all'interno della funzione "createPage (casper)" subito dopo "var page = request ('pagina web'). Create ();" aggiungi il seguente codice:

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

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

Quindi all'interno di "page.onResourceReceived = function onResourceReceived (risorsa) {" nella prima riga aggiungere:

 resetTimeout()

Fare lo stesso per "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Infine, su "page.onLoadFinished = function onLoadFinished (status) {" nella prima riga aggiungi:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Ed è tutto, spero che questo aiuti qualcuno nei guai come me. Questa soluzione è per casperjs ma funziona direttamente per Spooky.

In bocca al lupo !


0

questa è la mia soluzione ha funzionato per me.

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);
    }
});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.