2012-07-05 25 views
124

Estoy usando PhantomJS v1.4.1 para cargar algunas páginas web. No tengo acceso a su lado del servidor, solo recibo enlaces apuntando a ellos. Estoy usando una versión obsoleta de Phantom porque necesito apoyar Adobe Flash en esas páginas web.phantomjs no está esperando la carga de la página "completa"

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

Respuesta

12

Quizás pueda usar el onResourceRequested and onResourceReceived callbacks para detectar la carga asíncrona. He aquí un ejemplo del uso de estas devoluciones de llamada from their 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); 

Además, se puede ver en examples/netsniff.js para un ejemplo de trabajo.

+0

Pero en este caso no puedo usar una instancia de PhantomJS para cargar más de una página a la vez, ¿verdad? – nilfalse

+0

¿onResourceRequested se aplica a solicitudes AJAX/Cross Domain? ¿O se aplica solo a css, imágenes, etc.? – CMCDragonkai

+0

@CMCDragonkai Nunca lo he usado, pero basado en [esto] (https://github.com/ariya/phantomjs/wiki/Network-Monitoring) parece que incluye todas las solicitudes. Cita: 'Todas las solicitudes y respuestas de recursos pueden ser olidas usando onResourceRequested y onResourceReceived' – Supr

18

Usted podría tratar de 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); 
     } 
    }); 
} 
+3

Parece que no funcionaría con páginas web que usan cualquiera de las tecnologías push del servidor, ya que el recurso todavía estará en uso después de que se produzca onLoad. – nilfalse

+0

Haga cualquier controlador, ej. [poltergeist] (https://github.com/jonleighton/poltergeist), ¿tiene una función como esta? –

+0

¿Es posible usar waitFor para sondear todo el texto html y buscar una palabra clave definida? Traté de implementar esto, pero parece que el sondeo no se actualiza a la última fuente html descargada. – fpdragon

66

Otro enfoque es simplemente pregunta PhantomJS que esperar un poco después de que la página se ha cargado antes de hacer el render, según la regularidad rasterize.js ejemplo, pero con un tiempo de espera más largo para permitir que el JavaScript para terminar de cargar los 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 
    } 
}); 
+1

Sí, actualmente me atiendo a este enfoque. – nilfalse

+7

Debe aceptar la respuesta – alex88

+89

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, seguirá fallando. Tal ineficiencia y falta de fiabilidad es insoportable para el trabajo profesional. – CoDEmanX

13

En mi programa, yo uso un poco de lógica para juzgar si era onload: ver su solicitud de red, si no había ninguna solicitud nueva en los últimos 200ms, tre en ello onload.

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

yo preferiría comprobar periódicamente document.readyState de estado (https://developer.mozilla.org/en-US/docs/Web/API/document.readyState). Aunque este enfoque es un poco torpe, puede estar seguro de que dentro de la función onPageReady 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:

anidadas utilizando setTimeout en lugar de setInterval impide checkReadyState de "solapamiento" y las condiciones de carrera cuando su ejecución se prolonga por algunas razones aleatorias. setTimeout tiene 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).

+4

el comentario sobre setTimeout vs setInterval es genial. –

+0

'readyState' solo se activará una vez que el DOM se haya cargado por completo, sin embargo, cualquier elemento'