phantomjs no espera carga de página "completa"

137

Estoy usando PhantomJS v1.4.1 para cargar algunas páginas web. No tengo acceso a su lado del servidor, solo obtengo enlaces que apuntan a ellos. Estoy usando una versión obsoleta de Phantom porque necesito admitir Adobe Flash en esas páginas web.

El problema es que muchos sitios web están cargando su contenido asíncrono menor y es por eso que la devolución de llamada onLoadFinished de Phantom (análogo para onLoad en HTML) se activó demasiado temprano cuando aún no se ha cargado todo. ¿Alguien puede sugerir cómo puedo esperar la carga completa de una página web para hacer, por ejemplo, una captura de pantalla con todo el contenido dinámico como los anuncios?

nilfalse
fuente
3
Creo que es hora de aceptar una respuesta
spartikus

Respuestas:

76

Otro enfoque es simplemente pedirle a PhantomJS que espere un poco después de que la página se haya cargado antes de realizar el renderizado, según el ejemplo regular rasterize.js , pero con un tiempo de espera más largo para permitir que JavaScript termine de cargar recursos adicionales:

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 
    }
});
Rhunwicks
fuente
1
Sí, actualmente me quedé con este enfoque.
nilfalse
102
Es una solución horrible, lo siento (¡es culpa de PhantomJS!). Si espera un segundo completo, pero tarda 20 ms en cargarse, es una pérdida de tiempo completa (piense en trabajos por lotes), o si tarda más de un segundo, todavía fallará. Tal ineficiencia y falta de fiabilidad es insoportable para el trabajo profesional.
CodeManX
9
El verdadero problema aquí es que nunca se sabe cuándo JavaScript terminará de cargar la página y el navegador tampoco lo sabe. Imagina un sitio que tiene algún javascript cargando algo del servidor en bucle infinito. Desde el punto de vista del navegador: la ejecución de JavaScript nunca termina, ¿cuál es el momento en que quieres que phantomjs te diga que ha terminado? Este problema no se puede resolver en un caso genérico, excepto con la solución de espera de tiempo de espera y la esperanza de lo mejor.
Maxim Galushka
55
¿Sigue siendo la mejor solución a partir de 2016? Parece que deberíamos poder hacerlo mejor que esto.
Adam Thompson, el
66
Si tiene el control del código que está intentando leer, puede llamar a la llamada fantasma js explícitamente: phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith
52

Prefiero verificar periódicamente el document.readyStateestado ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Aunque este enfoque es un poco torpe, puede estar seguro de que la onPageReadyfunción interna está utilizando un documento completamente cargado.

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

Explicación adicional:

El uso de anidado en setTimeoutlugar de setIntervalevita la checkReadyState"superposición" y las condiciones de carrera cuando su ejecución se prolonga por algunas razones aleatorias. setTimeouttiene un retraso predeterminado de 4 ms ( https://stackoverflow.com/a/3580085/1011156 ), por lo que el sondeo activo no afectará drásticamente el rendimiento del programa.

document.readyState === "complete"significa que el documento está completamente cargado con todos los recursos ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

Mateusz Charytoniuk
fuente
44
El comentario sobre setTimeout vs setInterval es genial.
Gal Bracha
1
readyStateserá único desencadenante una vez que el DOM se ha cargado completamente, sin embargo ningún <iframe>elementos todavía puede ser de carga por lo que en realidad no contesta la pregunta original
CodingIntrigue
1
@rgraham No es ideal, pero creo que solo podemos hacer mucho con estos renderizadores. Habrá casos extremos en los que simplemente no sabrás si algo está completamente cargado. Piense en una página donde el contenido se retrasa, a propósito, por uno o dos minutos. No es razonable esperar que el proceso de renderizado permanezca y espere una cantidad indefinida de tiempo. Lo mismo ocurre con el contenido cargado de fuentes externas que puede ser lento.
Brandon Elliott
3
Esto no considera ninguna carga de JavaScript después de que DOM se cargue por completo, como con Backbone / Ember / Angular.
Adam Thompson, el
1
No funcionó en absoluto para mí. Puede que readyState complete se haya disparado, pero la página estaba en blanco en este momento.
Steve Staple
21

Puede probar una combinación de los ejemplos waitfor y 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);
        }
    });
}
Rhunwicks
fuente
3
Parece que no funcionaría con páginas web, que usan cualquiera de las tecnologías de inserción del servidor, ya que los recursos seguirán en uso después de que ocurra onLoad.
nilfalse
¿Alguno de los conductores, por ejemplo. poltergeist , ¿tiene una característica como esta?
Jared Beck
¿Es posible usar waitFor para sondear todo el texto html y buscar una palabra clave definida? Intenté implementar esto pero parece que el sondeo no se actualiza a la última fuente html descargada.
fpdragon
14

Tal vez pueda usar las devoluciones de llamada onResourceRequestedyonResourceReceived para detectar la carga asincrónica. Aquí hay un ejemplo del uso de esas devoluciones de llamada de su documentación :

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

Además, puede buscar examples/netsniff.jsun ejemplo de trabajo.

Supr
fuente
Pero en este caso no puedo usar una instancia de PhantomJS para cargar más de una página a la vez, ¿verdad?
nilfalse
¿OnResourceRequested se aplica a las solicitudes de AJAX / Cross Domain? ¿O se aplica solo a me gusta css, imágenes, etc.?
CMCDragonkai
@CMCDragonkai Nunca lo he usado yo mismo, pero en base a esto parece que incluye todas las solicitudes. Cita:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr
He usado este método con renderizado PhantomJS a gran escala y funciona bastante bien. Necesita mucha inteligencia para rastrear las solicitudes y observar si fallan o si se agota el tiempo de espera. Más información: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty
14

Aquí hay una solución que espera a que se completen todas las solicitudes de recursos. Una vez completado, registrará el contenido de la página en la consola y generará una captura de pantalla de la página renderizada.

Aunque esta solución puede servir como un buen punto de partida, he observado que falla, por lo que definitivamente no es una solución completa.

No tuve mucha suerte usando document.readyState.

Fui influenciado por el ejemplo waitfor.js que se encuentra en la página de ejemplos 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);
});
Dave
fuente
Dio el visto bueno, pero usó setTimeout con 10, en lugar de intervalo
GDmac
Debe verificar que response.stage es igual a 'end' antes de eliminarlo de la matriz de solicitudes, de lo contrario, podría eliminarse prematuramente.
Reembolsar el
Esto no funciona si su página web carga el DOM dinámicamente
Buddy
13

En mi programa, uso algo de lógica para juzgar si estaba en carga: viendo su solicitud de red, si no hubo una nueva solicitud en los últimos 200 ms, la trato como en carga.

Use esto, después de 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;
        }
    }
}
deemstone
fuente
11

Encontré este enfoque útil en algunos casos:

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

Que si usted es dueño de la página, coloque algún script dentro:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
Brankodd
fuente
Esto parece una solución muy buena, sin embargo, no pude obtener ningún mensaje de registro de mi página HTML / JavaScript para pasar por phantomJS ... el evento onConsoleMessage nunca se activó mientras pude ver los mensajes perfectamente en la consola del navegador, y No tengo ni idea de por qué.
Dirk
1
Necesitaba page.onConsoleMessage = function (msg) {};
Andy Balaam
5

Encontré esta solución útil en una aplicación NodeJS. Lo uso solo en casos desesperados porque inicia un tiempo de espera para esperar la carga de la página completa.

El segundo argumento es la función de devolución de llamada que se llamará una vez que la respuesta esté lista.

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);
Manu
fuente
3

Esta es una implementación de la respuesta de Supr. También usa setTimeout en lugar de setInterval como lo sugirió Mateusz Charytoniuk.

Phantomjs saldrá en 1000 ms cuando no haya ninguna solicitud o respuesta.

// 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();
Dayong
fuente
3

Este es el código que 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);
  });

Básicamente, dado el hecho de que se supone que debes saber que la página se descarga por completo cuando aparece un elemento determinado en el DOM. Entonces el guión esperará hasta que esto suceda.

Rocco Musolino
fuente
3

Utilizo una mezcla personal del waitfor.jsejemplo phantomjs .

Este es mi main.jsarchivo:

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

Y el lib/waitFor.jsarchivo (que es solo una copia y pega de la waifFor()función del waitfor.jsejemplo 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
}

Este método no es asíncrono, pero al menos estoy seguro de que todos los recursos se cargaron antes de intentar usarlos.

Daishi
fuente
2

Esta es una vieja pregunta, pero como estaba buscando la carga de la página completa, pero para Spookyjs (que usa casperjs y phantomjs) y no encontré mi solución, creé mi propio script para eso, con el mismo enfoque que el usuario deemstone. Lo que hace este enfoque es que, durante un período de tiempo determinado, si la página no recibió o inició alguna solicitud, finalizará la ejecución.

En el archivo casper.js (si lo instaló globalmente, la ruta sería algo así como /usr/local/lib/node_modules/casperjs/modules/casper.js) agregue las siguientes líneas:

En la parte superior del archivo con todos los vars globales:

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

Luego dentro de la función "createPage (casper)" justo después de "var page = require ('webpage'). Create ();" agregue el siguiente código:

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

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

Luego, dentro de "page.onResourceReceived = function onResourceReceived (resource) {" en la primera línea, agregue:

 resetTimeout()

Haga lo mismo para "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Finalmente, en "page.onLoadFinished = function onLoadFinished (estado) {" en la primera línea, agregue:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Y eso es todo, espero que esto ayude a alguien en problemas como yo. Esta solución es para casperjs pero funciona directamente para Spooky.

Buena suerte !

fdnieves
fuente
0

Esta es mi solución, funcionó para mí.

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