phantomjs não está esperando pelo carregamento de página “completo”

Estou usando o PhantomJS v1.4.1 para carregar algumas páginas da web. Eu não tenho access ao servidor deles, acabei de receber links apontando para eles. Estou usando uma versão obsoleta do Phantom porque preciso dar suporte ao Adobe Flash nessas páginas da web.

O problema é que muitos sites estão carregando seu conteúdo secundário asynchronous e é por isso que o retorno de chamada onLoadFinished do Phantom (analógico para onLoad em HTML) disparou muito cedo quando nem tudo ainda foi carregado. Alguém pode sugerir como posso esperar pelo carregamento total de uma página da Web, por exemplo, uma captura de canvas com todo o conteúdo dynamic, como anúncios?

Outra abordagem é apenas pedir ao PhantomJS para esperar um pouco depois que a página foi carregada antes de renderizar, conforme o exemplo regular do rasterize.js , mas com um tempo limite maior para permitir que o JavaScript termine de carregar resources adicionais:

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

Eu preferiria verificar periodicamente o status document.readyState ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Embora essa abordagem seja um pouco desajeitada, você pode ter certeza de que, dentro da function onPageReady , está usando um documento totalmente carregado.

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

Explicação adicional:

O uso de setTimeout nested em vez de setInterval evita checkReadyState de “sobreposição” e condições de corrida quando sua execução é prolongada por alguns motivos randoms. setTimeout tem um atraso padrão de 4ms ( https://stackoverflow.com/a/3580085/1011156 ) para que a pesquisa ativa não afete drasticamente o desempenho do programa.

document.readyState === "complete" significa que o documento está completamente carregado com todos os resources ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

Você poderia tentar uma combinação dos exemplos 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 (eg: "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 (eg: "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); } }); } 

Talvez você possa usar os retornos de chamada onResourceReceived e onResourceReceived para detectar o carregamento asynchronous. Veja um exemplo de como usar esses retornos de chamada de sua documentação :

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

Além disso, você pode ver examples/netsniff.js para um exemplo funcional.

No meu programa, eu uso alguma lógica para julgar se era onload: observando sua requisição de rede, se não houve uma nova requisição nos últimos 200ms, eu a trato onload.

Use isso, depois 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; } } } 

Aqui está uma solução que aguarda a conclusão de todas as solicitações de resources. Depois de concluído, ele registrará o conteúdo da página no console e gerará uma captura de canvas da página renderizada.

Embora esta solução possa servir como um bom ponto de partida, tenho observado que ela falha, então definitivamente não é uma solução completa!

Eu não tive muita sorte usando document.readyState .

Fui influenciado pelo exemplo waitfor.js encontrado na página de exemplos do 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); }); 

Eu achei essa abordagem útil em alguns casos:

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

Do que se você possui a página coloque algum script dentro:

  

Eu encontrei esta solução útil em um aplicativo NodeJS. Eu o uso apenas em casos desesperados porque ele lança um tempo limite para aguardar o carregamento total da página.

O segundo argumento é a function de retorno de chamada que será chamada assim que a resposta estiver pronta.

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

Esta é uma implementação da resposta de Supr. Também usa setTimeout em vez de setInterval como Mateusz Charytoniuk sugeriu.

O Phantomjs sairá em 1000 ms quando não houver qualquer solicitação ou resposta.

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

Este código eu 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); }); 

Basicamente dado o fato de que você deveria saber que a página está completa baixada quando um determinado elemento aparece no DOM. Então o roteiro vai esperar até que isso aconteça.

Esta é uma pergunta antiga, mas desde que eu estava procurando por carga de página completa, mas para o Spookyjs (que usa casperjs e phantomjs) e não encontrei minha solução, fiz meu próprio script para isso, com a mesma abordagem que o deemstone do usuário. O que essa abordagem faz é, por um determinado período de tempo, se a página não receber ou iniciar qualquer solicitação, terminará a execução.

No arquivo casper.js (se você o instalou globalmente, o caminho seria algo como /usr/local/lib/node_modules/casperjs/modules/casper.js) adicione as seguintes linhas:

No topo do arquivo com todas as variantes globais:

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

Em seguida, dentro da function “createPage (casper)” logo após “var page = require (‘webpage’). Create ();” adicione o seguinte código:

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

Em seguida, dentro de “page.onResourceReceived = function onResourceReceived (resource) {” na primeira linha, adicione:

  resetTimeout() 

Faça o mesmo para “page.onResourceRequested = function onResourceRequested (requestData, request) {”

Finalmente, em “page.onLoadFinished = function onLoadFinished (status) {” na primeira linha, adicione:

  if(!reqResFinished) { return } reqResFinished = false 

E é isso, espero que isso ajude alguém com problemas como eu estava. Esta solução é para casperjs, mas funciona diretamente para o Spooky.

Boa sorte !

Eu uso uma mistura pessoal do exemplo do phantomjs waitfor.js .

Este é o meu arquivo 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); } }); 

E o arquivo lib/waitFor.js (que é apenas uma cópia e uma pasta da function waifFor() do exemplo phantomjs waitfor.js ):

 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 } 

Esse método não é asynchronous, mas pelo menos eu tenho certeza de que todos os resources foram carregados antes de tentar usá-los.

esta é a minha solução, funcionou para mim.

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