phantomjs não está esperando o carregamento da página "completo"

137

Estou usando o PhantomJS v1.4.1 para carregar algumas páginas da web. Não tenho acesso ao lado do servidor, apenas obtendo links apontando para eles. Estou usando a versão obsoleta do Phantom porque preciso oferecer suporte ao Adobe Flash nessas páginas da web.

O problema é que muitos sites estão carregando seu conteúdo menor assíncrono e é por isso que o retorno de chamada onLoadFinished do Phantom (analógico para onLoad em HTML) foi acionado muito cedo, quando nem tudo ainda estava carregado. Alguém pode sugerir como posso esperar que o carregamento completo de uma página da Web faça, por exemplo, uma captura de tela com todo o conteúdo dinâmico, como anúncios?

nilfalse
fonte
3
Eu acho que é hora de aceitar uma resposta
spartikus

Respostas:

76

Outra abordagem é pedir ao PhantomJS que aguarde um pouco após o carregamento da página antes de fazer a renderização, conforme o exemplo regular do rasterize.js , mas com um tempo limite mais longo para permitir que o JavaScript conclua o carregamento de recursos 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 
    }
});
rhunwicks
fonte
1
Sim, atualmente eu me apeguei a essa abordagem.
Nilfalse
102
É uma solução horrível, desculpe (a culpa é do PhantomJS!). Se você esperar um segundo inteiro, mas leva 20ms para carregar, é um completo desperdício de tempo (pense em trabalhos em lotes) ou, se demorar mais de um segundo, ainda falhará. Essa ineficiência e falta de confiabilidade são insuportáveis ​​para o trabalho profissional.
CodeManX
9
O verdadeiro problema aqui é que você nunca sabe quando o javascript terminará de carregar a página e o navegador também não o conhece. Imagine um site que tem algum javascript carregando algo do servidor em loop infinito. Do ponto de vista do navegador - a execução do javascript nunca termina, então, em que momento você deseja que o phantomjs diga que ele terminou? Esse problema é insolúvel em casos genéricos, exceto com a espera pela solução de tempo limite e a esperança do melhor.
Maxim Galushka
5
Essa ainda é a melhor solução a partir de 2016? Parece que deveríamos ser melhores que isso.
21426 Adam Thompson #
6
Se você está no controle do código que está tentando ler, pode chamar a chamada fantasma js explicitamente: phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith
52

Prefiro verificar periodicamente o document.readyStatestatus ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Embora essa abordagem seja um pouco desajeitada, você pode ter certeza de que a onPageReadyfunção interna está usando 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:

Usando aninhado setTimeout vez de setIntervalimpedir a checkReadyState"sobreposição" e condições de corrida quando sua execução é prolongada por alguns motivos aleatórios. setTimeouttem um atraso padrão de 4ms ( https://stackoverflow.com/a/3580085/1011156 ), portanto a pesquisa ativa não afetará drasticamente o desempenho do programa.

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

Mateusz Charytoniuk
fonte
4
o comentário sobre setTimeout vs setInterval é ótimo.
Gal Bracha
1
readyStatesó gatilho assim que o DOM tenha sido totalmente carregada, no entanto quaisquer <iframe>elementos podem ainda ser carregamento por isso realmente não responder à pergunta original
CodingIntrigue
1
@rgraham Não é o ideal, mas acho que só podemos fazer muito com esses renderizadores. Haverá casos extremos onde você simplesmente não saberá se algo está totalmente carregado. Pense em uma página em que o conteúdo está atrasado, de propósito, por um ou dois minutos. Não é razoável esperar que o processo de renderização fique parado e espere uma quantidade indefinida de tempo. O mesmo vale para o conteúdo carregado de fontes externas que pode ser lento.
Brandon Elliott
3
Isso não considera nenhum carregamento de JavaScript após o carregamento completo do DOM, como no Backbone / Ember / Angular.
Adam Thompson
1
Não funcionou para mim. readyState complete pode ter sido acionado, mas a página estava em branco neste momento.
Steve Staple
21

Você pode 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 (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
fonte
3
Parece que não funcionaria com páginas da Web, que usam qualquer uma das tecnologias push do servidor, pois o recurso ainda estará em uso após a ocorrência do onLoad.
Nilfalse
Faça algum driver, por exemplo. poltergeist , tem um recurso como este?
Jared Beck
É possível usar waitFor para pesquisar todo o texto html e procurar uma palavra-chave definida? Tentei implementar isso, mas parece que a pesquisa não é atualizada para a fonte html baixada mais recente.
Fpdragon
14

Talvez você possa usar os retornos de chamada onResourceRequestedeonResourceReceived para detectar carregamento assíncrono. Aqui está um exemplo de como usar esses retornos de chamada da 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 procurar examples/netsniff.jsum exemplo de trabalho.

Supr
fonte
Mas, neste caso, não posso usar uma instância do PhantomJS para carregar mais de uma página por vez, certo?
Nilfalse 10/07
OnResourceRequested se aplica a solicitações AJAX / entre domínios? Ou se aplica apenas a CSS, imagens, etc.?
precisa saber é o seguinte
@CMCDragonkai Eu nunca o usei, mas com base nisso , parece que inclui todos os pedidos. Citação:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr 24/09
Eu usei esse método com a renderização em grande escala do PhantomJS e funciona muito bem. Você precisa de muita inteligência para rastrear solicitações e observar se elas falham ou atingem o tempo limite. Mais informações: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty
14

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

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

Não tive muita sorte usando document.readyState.

Fui influenciado pelo exemplo do 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);
});
Dave
fonte
Deu um polegar para cima, mas setTimeout usados com 10, em vez de intervalo
GDmac
Você deve verificar se response.stage é igual a 'end' antes de removê-lo da matriz de solicitações, caso contrário, poderá ser removido prematuramente.
Reimund
Isso não funciona se suas cargas da página da Internet DOM dinamicamente
camarada
13

No meu programa, uso alguma lógica para julgar se foi onload: observando sua solicitação de rede, se não houve nova solicitação nos últimos 200ms, trato-a 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;
        }
    }
}
deemstone
fonte
11

Achei essa abordagem útil em alguns casos:

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

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

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
Brankodd
fonte
Parece uma solução alternativa muito boa, no entanto, não foi possível que nenhuma mensagem de log da minha página HTML / JavaScript passasse pelo phantomJS ... o evento onConsoleMessage nunca foi acionado enquanto eu podia ver as mensagens perfeitamente no console do navegador e Eu não tenho idéia do porquê.
Dirk
1
Eu precisava de page.onConsoleMessage = function (msg) {};
Andy Balaam
5

Encontrei esta solução útil em um aplicativo NodeJS. Eu o uso apenas em casos desesperados, porque inicia um tempo limite para aguardar o carregamento completo da página.

O segundo argumento é a função 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);
Manu
fonte
3

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

O Phantomjs será encerrado em 1000ms quando não houver nenhuma 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();
Dayong
fonte
3

Este é o código que 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, considerando que você deveria saber que a página é baixada por completo quando um determinado elemento aparece no DOM. Portanto, o script vai esperar até que isso aconteça.

Rocco Musolino
fonte
3

Eu uso uma mistura pessoal do exemplo phantomjswaitfor.js .

Este é o meu main.jsarquivo:

'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 lib/waitFor.jsarquivo (que é apenas uma cópia e cola da waifFor()função do waitfor.jsexemplo 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
}

Esse método não é assíncrono, mas pelo menos tenho certeza de que todos os recursos foram carregados antes de tentar usá-los.

Daishi
fonte
2

Essa é uma pergunta antiga, mas como eu estava procurando pelo carregamento da página inteira, mas pelo Spookyjs (que usa casperjs e phantomjs) e não encontrou minha solução, criei meu próprio script para isso, com a mesma abordagem que o usuário deemstone. O que essa abordagem faz é, por um determinado período de tempo, se a página não receber ou iniciar nenhuma solicitação, ela encerrará a execução.

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

Na parte superior do arquivo, com todos os vars globais:

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

Em seguida, dentro da função "createPage (casper)" logo após "var page = require ('página da web'). 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) {"

Por fim, em "page.onLoadFinished = function onLoadFinished (status) {" na primeira linha, adicione:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

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

Boa sorte !

fdnieves
fonte
0

esta é a minha solução que 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('</body>','<script>window.onload = function(){console.log(\'hey lets take screenshot\');}</script></body>'), address);
    }
});
Tom
fonte