javascript - phantomjs venter ikke på at siden skal være helt lastet


Jeg bruker PhantomJS v1.4.1 for å laste noen nettsider. Jeg har ikke tilgang til server-siden deres, jeg får bare lenker som peker til dem. Jeg bruker en utdatert versjon av Phantom fordi jeg må støtte Adobe Flash på de nettsidene.

Problemet er at mange nettsteder laster sitt mindre innhold asynkront, og derfor blir Phantoms onLoadFinished-callback (analogen til onLoad i HTML) utløst for tidlig når ikke alt fortsatt har blitt lastet. Kan noen foreslå hvordan jeg kan vente på at en nettside er fullstendig lastet for å for eksempel ta et skjermbilde med alt dynamisk innhold som annonser?

Svar #1

En annen tilnærming er å bare be PhantomJS om å vente litt etter at siden har lastet før man gjør gjengivelsen, som i det vanlige rasterize.js-eksemplet, men med en lengre tidsavbrudd for å la JavaScript ferdigstille lasting av flere ressurser:

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

Svar #2

Jeg vil heller periodisk sjekke document.readyState-status (https://developer.mozilla.org/en-US/docs/Web/API/document.readyState). Selv om denne metoden er litt klønete, kan du være sikker på at inne i onPageReady-funksjonen bruker du et fullstendig lastet dokument.

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

Ytterligere forklaring:

Å bruke nestede setTimeout i stedet for setInterval forhindrer at checkReadyState «overlapper» og race-tilstander når utførelsen blir forsinket av tilfeldige årsaker. setTimeout har en standard forsinkelse på 4 ms (https://stackoverflow.com/a/3580085/1011156), så aktiv polling vil ikke drastisk påvirke programmets ytelse.

document.readyState === "complete" betyr at dokumentet er fullstendig lastet med alle ressurser (https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness).

OPPDATERING 2022: Jeg laget dette svaret for 8 år siden, og jeg har ikke brukt PhantomJS siden da. Det er svært sannsynlig at det ikke vil fungere nå i noen tilfeller. Nå tror jeg også det ikke er mulig å lage en løsning som passer for alle, for å være helt sikker på at siden er lastet. Dette er fordi noen sider kan laste flere ressurser etter at dokumentet er klart. For eksempel kan det være noen JS-kode på nettsiden som venter til dokumentet er klart, og deretter laster noen flere ressurser (etter at dokumenttilstanden endres til ready) - i dette tilfellet vil onPageReady bli utløst, og deretter vil siden begynne å laste flere ressurser igjen.

Jeg synes fortsatt at koden ovenfor er et godt utgangspunkt og kan fungere i de fleste tilfeller, men det kan også være nødvendig å lage spesifikke løsninger for å håndtere spesifikke nettsteder.

Svar #3

Du kan prøve en kombinasjon av eksemplene waitfor og 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);
        }
    });
}

Svar #4

Her er en løsning som venter på at alle ressursforespørsler skal fullføres. Når det er fullført, vil den logge sideinnholdet til konsollen og generere et skjermbilde av den gjengitte siden.

Selv om denne løsningen kan fungere som et godt utgangspunkt, har jeg observert at den feiler, så den er definitivt ikke en fullstendig løsning!

Jeg hadde ikke mye hell med å bruke document.readyState.

Jeg ble påvirket av waitfor.js-eksemplet som ble funnet på PhantomJS-eksempelsiden.

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);
  if (index > -1 && response.stage === 'end') {
    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);
});

Svar #5

Kanskje du kan bruke onResourceRequested og onResourceReceived tilbakekallingsfunksjoner for å oppdage asynkron lasting. Her er et eksempel på hvordan du bruker disse tilbakekallingsfunksjonene fra dokumentasjonen deres:

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

Du kan også se på examples/netsniff.js for et fungerende eksempel.

Svar #6

I programmet mitt bruker jeg litt logikk for å vurdere om det var onload: ved å overvåke nettverksforespørselen, hvis det ikke var noen ny forespørsel de siste 200 ms, betrakter jeg det som onload.

Bruk dette, etter 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;
        }
    }
}

Svar #7

Jeg fant denne tilnærmingen nyttig i noen tilfeller:

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

Da, hvis du eier siden, sett inn noe skript:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>

Relaterte artikler