Phantomjs warten nicht auf das Laden der vollständigen Seite

136

Ich benutze PhantomJS v1.4.1, um einige Webseiten zu laden. Ich habe keinen Zugriff auf ihre Serverseite, sondern erhalte nur Links, die auf sie verweisen. Ich verwende eine veraltete Version von Phantom, da ich Adobe Flash auf diesen Webseiten unterstützen muss.

Das Problem ist, dass viele Websites ihre geringfügigen Inhalte asynchron laden. Aus diesem Grund wurde der onLoadFinished-Rückruf von Phantom (analog für onLoad in HTML) zu früh ausgelöst, wenn noch nicht alles geladen ist. Kann jemand vorschlagen, wie ich warten kann, bis eine Webseite vollständig geladen ist, um beispielsweise einen Screenshot mit allen dynamischen Inhalten wie Anzeigen zu erstellen?

nilfalse
quelle
3
Ich denke, es ist Zeit, eine Antwort zu akzeptieren
Spartikus

Antworten:

75

Ein anderer Ansatz besteht darin, PhantomJS zu bitten, nach dem Laden der Seite etwas zu warten, bevor das Rendern ausgeführt wird, wie im regulären Beispiel rasterize.js , jedoch mit einer längeren Zeitüberschreitung, damit das JavaScript das Laden zusätzlicher Ressourcen beenden kann:

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
quelle
1
Ja, derzeit habe ich mich an diesen Ansatz gehalten.
Nilfalse
102
Es ist eine schreckliche Lösung, sorry (es ist PhantomJSs Schuld!). Wenn Sie eine volle Sekunde warten, das Laden jedoch 20 ms dauert, ist dies eine reine Zeitverschwendung (denken Sie an Stapeljobs), oder wenn es länger als eine Sekunde dauert, schlägt es immer noch fehl. Eine solche Ineffizienz und Unzuverlässigkeit ist für die professionelle Arbeit unerträglich.
CodeManX
9
Das eigentliche Problem hierbei ist, dass Sie nie wissen, wann Javascript das Laden der Seite beendet und der Browser es auch nicht weiß. Stellen Sie sich eine Site vor, auf der Javascript etwas vom Server in einer Endlosschleife lädt. Aus Browsersicht endet die Ausführung von Javascript nie. In welchem ​​Moment möchten Sie also, dass Phantomjs Ihnen mitteilen, dass die Javascript-Ausführung abgeschlossen ist? Dieses Problem ist im allgemeinen Fall nicht lösbar, außer wenn Sie auf eine Timeout-Lösung warten und auf das Beste hoffen.
Maxim Galushka
5
Ist dies ab 2016 immer noch die beste Lösung? Es scheint, wir sollten es besser machen können.
Adam Thompson
6
Wenn Sie die Kontrolle über den Code haben, den Sie lesen möchten, können Sie den Phantom-Js-Rückruf explizit zurückrufen
Andy Smith
52

Ich würde lieber regelmäßig nach dem document.readyStateStatus suchen ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Obwohl dieser Ansatz etwas umständlich ist, können Sie sicher sein, dass Sie innerhalb der onPageReadyFunktion ein vollständig geladenes Dokument verwenden.

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

Zusätzliche Erklärung:

Die Verwendung von verschachtelten setTimeoutanstelle von setIntervalverhindert, dass sich checkReadyState"überlappen" und Rennbedingungen auftreten, wenn die Ausführung aus zufälligen Gründen verlängert wird. setTimeouthat eine Standardverzögerung von 4 ms ( https://stackoverflow.com/a/3580085/1011156 ), sodass aktive Abfragen die Programmleistung nicht drastisch beeinträchtigen.

document.readyState === "complete"bedeutet, dass das Dokument vollständig mit allen Ressourcen geladen ist ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

Mateusz Charytoniuk
quelle
4
Der Kommentar zu setTimeout vs setInterval ist großartig.
Gal Bracha
1
readyStatewird erst ausgelöst, wenn das DOM vollständig geladen wurde. <iframe>Möglicherweise werden jedoch noch Elemente geladen, sodass die ursprüngliche Frage
CodingIntrigue
1
@rgraham Es ist nicht ideal, aber ich denke, wir können nur so viel mit diesen Renderern machen. Es wird Randfälle geben, in denen Sie einfach nicht wissen, ob etwas vollständig geladen ist. Denken Sie an eine Seite, auf der der Inhalt absichtlich um ein oder zwei Minuten verzögert wird. Es ist unvernünftig zu erwarten, dass der Renderprozess herumsteht und auf unbestimmte Zeit wartet. Gleiches gilt für Inhalte, die aus externen Quellen geladen werden und möglicherweise langsam sind.
Brandon Elliott
3
Dies berücksichtigt kein Laden von JavaScript nach dem vollständigen Laden von DOM, z. B. mit Backbone / Ember / Angular.
Adam Thompson
1
Hat bei mir überhaupt nicht funktioniert. readyState complete wurde möglicherweise ausgelöst, aber die Seite war zu diesem Zeitpunkt leer.
Steve Staple
21

Sie können eine Kombination aus Waitfor- und Raster-Beispielen ausprobieren:

/**
 * 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
quelle
3
Es scheint, als würde es mit Webseiten, die Server-Push-Technologien verwenden, nicht funktionieren, da die Ressource nach dem Auftreten von onLoad weiterhin verwendet wird.
Nilfalse
Machen Sie irgendwelche Fahrer, z. Poltergeist , haben Sie eine Funktion wie diese?
Jared Beck
Ist es möglich, mit waitFor den gesamten HTML-Text abzufragen und nach einem definierten Schlüsselwort zu suchen? Ich habe versucht, dies zu implementieren, aber es scheint, dass die Abfrage nicht auf die zuletzt heruntergeladene HTML-Quelle aktualisiert wird.
Fpdragon
14

Möglicherweise können Sie die Rückrufe onResourceRequestedund verwendenonResourceReceived , um das asynchrone Laden zu erkennen. Hier ist ein Beispiel für die Verwendung dieser Rückrufe aus ihrer Dokumentation :

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

Sie können sich auch examples/netsniff.jsein funktionierendes Beispiel ansehen .

Supr
quelle
Aber in diesem Fall kann ich nicht eine Instanz von PhantomJS verwenden, um mehr als eine Seite gleichzeitig zu laden, oder?
Nilfalse
Gilt onResourceRequested für AJAX / Domainübergreifende Anforderungen? Oder gilt es nur, um CSS, Bilder usw. zu mögen?
CMCDragonkai
@CMCDragonkai Ich habe es selbst nie benutzt, sondern basiert auf dieser scheint es , wie es alle Anforderungen enthält. Zitat:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Supr
Ich habe diese Methode mit PhantomJS-Rendering in großem Maßstab verwendet und sie funktioniert recht gut. Sie benötigen eine Menge Smarts, um Anforderungen zu verfolgen und zu beobachten, ob sie fehlschlagen oder eine Zeitüberschreitung auftreten. Weitere Informationen: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty
14

Hier ist eine Lösung, die darauf wartet, dass alle Ressourcenanforderungen abgeschlossen sind. Sobald der Vorgang abgeschlossen ist, wird der Seiteninhalt in der Konsole protokolliert und ein Screenshot der gerenderten Seite erstellt.

Obwohl diese Lösung als guter Ausgangspunkt dienen kann, habe ich festgestellt, dass sie fehlschlägt, sodass sie definitiv keine vollständige Lösung ist!

Ich hatte nicht viel Glück damit document.readyState.

Ich wurde von dem Beispiel waitfor.js beeinflusst, das auf der Beispielseite von phantomjs zu finden ist .

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
quelle
Hat einen Daumen hoch gegeben, aber setTimeout mit 10 anstelle von Intervall verwendet
GDmac
Sie sollten überprüfen, ob response.stage gleich 'end' ist, bevor Sie es aus dem Anforderungsarray entfernen. Andernfalls wird es möglicherweise vorzeitig entfernt.
Reimund
Dies funktioniert nicht, wenn Ihre Webseite das DOM dynamisch lädt
Buddy
13

In meinem Programm verwende ich eine Logik, um zu beurteilen, ob es onload war: Wenn ich die Netzwerkanforderung beobachte und in den letzten 200 ms keine neue Anforderung vorhanden war, behandle ich sie onload.

Verwenden Sie dies nach 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
quelle
11

Ich fand diesen Ansatz in einigen Fällen nützlich:

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

Wenn Sie die Seite besitzen, geben Sie ein Skript ein:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
Brankodd
quelle
Dies sieht nach einer wirklich guten Lösung aus. Ich konnte jedoch keine Protokollnachricht von meiner HTML / JavaScript-Seite erhalten, die durch phantomJS geleitet werden konnte. Das Ereignis onConsoleMessage wurde nie ausgelöst, während ich die Nachrichten auf der Browserkonsole perfekt sehen konnte Ich habe keine Ahnung warum.
Dirk
1
Ich brauchte page.onConsoleMessage = function (msg) {};
Andy Balaam
5

Ich fand diese Lösung in einer NodeJS-App nützlich. Ich benutze es nur in verzweifelten Fällen, weil es eine Zeitüberschreitung auslöst, um auf das vollständige Laden der Seite zu warten.

Das zweite Argument ist die Rückruffunktion, die aufgerufen wird, sobald die Antwort fertig ist.

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
quelle
3

Dies ist eine Implementierung der Antwort von Supr. Außerdem wird setTimeout anstelle von setInterval verwendet, wie von Mateusz Charytoniuk vorgeschlagen.

Phantomjs werden in 1000 ms beendet, wenn keine Anfrage oder Antwort vorliegt.

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

Dies ist der Code, den ich benutze:

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

Grundsätzlich sollte man wissen, dass die Seite vollständig heruntergeladen ist, wenn ein bestimmtes Element im DOM angezeigt wird. Das Skript wird also warten, bis dies geschieht.

Rocco Musolino
quelle
3

Ich verwende eine persönliche Mischung aus dem Phantomjs- waitfor.jsBeispiel .

Das ist meine main.jsDatei:

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

Und die lib/waitFor.jsDatei (die nur ein Kopieren und Einfügen der waifFor()Funktion aus dem Phantomjs- waitfor.jsBeispiel ist ):

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
}

Diese Methode ist nicht asynchron, aber ich bin mir zumindest sicher, dass alle Ressourcen geladen wurden, bevor ich versuche, sie zu verwenden.

Daishi
quelle
2

Dies ist eine alte Frage, aber da ich nach dem vollständigen Laden von Seiten gesucht habe, aber nach Spookyjs (das casperjs und phantomjs verwendet) und meine Lösung nicht gefunden habe, habe ich dafür mein eigenes Skript erstellt, mit dem gleichen Ansatz wie der Benutzer deemstone. Dieser Ansatz bewirkt, dass die Seite für eine bestimmte Zeitspanne die Ausführung beendet, wenn sie keine Anforderung erhalten oder gestartet hat.

Fügen Sie in der Datei casper.js (wenn Sie sie global installiert haben, lautet der Pfad etwa /usr/local/lib/node_modules/casperjs/modules/casper.js) die folgenden Zeilen:

Am Anfang der Datei mit allen globalen Variablen:

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

Dann innerhalb der Funktion "createPage (casper)" direkt nach "var page = require ('webpage'). Create ();" Fügen Sie den folgenden Code hinzu:

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

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

Fügen Sie dann in "page.onResourceReceived = function onResourceReceived (resource) {" in der ersten Zeile Folgendes hinzu:

 resetTimeout()

Machen Sie dasselbe für "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Schließlich fügen Sie in der ersten Zeile unter "page.onLoadFinished = function onLoadFinished (status) {" Folgendes hinzu:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

Und das war's, hoffe, dieser hilft jemandem in Schwierigkeiten wie ich. Diese Lösung ist für Casperjs, funktioniert aber direkt für Spooky.

Viel Glück !

fdnieves
quelle
0

Das ist meine Lösung, die für mich funktioniert hat.

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
quelle