phantomjs nie czekają na „pełne” załadowanie strony

137

Używam PhantomJS v1.4.1 do ładowania niektórych stron internetowych. Nie mam dostępu do ich serwera, po prostu otrzymuję odsyłacze do nich. Używam przestarzałej wersji Phantoma, ponieważ muszę obsługiwać Adobe Flash na tych stronach internetowych.

Problem polega na tym, że wiele stron internetowych ładuje mniejszą zawartość asynchronicznie i dlatego wywołanie zwrotne onLoadFinished Phantoma (analog dla onLoad w HTML) uruchomiło się zbyt wcześnie, gdy nie wszystko zostało jeszcze załadowane. Czy ktoś może zasugerować, jak mogę czekać na pełne załadowanie strony internetowej, aby na przykład zrobić zrzut ekranu z całą zawartością dynamiczną, taką jak reklamy?

nilfalse
źródło
3
Myślę, że czas przyjąć odpowiedź
spartikus

Odpowiedzi:

76

Innym podejściem jest po prostu poproszenie PhantomJS, aby poczekał chwilę po załadowaniu strony przed wykonaniem renderowania, jak w zwykłym przykładzie rasterize.js , ale z dłuższym limitem czasu, aby umożliwić JavaScriptowi zakończenie ładowania dodatkowych zasobów:

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
źródło
1
Tak, obecnie trzymam się tego podejścia.
nilfalse
102
To okropne rozwiązanie, przepraszam (to wina PhantomJS!). Jeśli poczekasz pełną sekundę, ale załadowanie zajmuje 20 ms, jest to całkowita strata czasu (pomyśl o zadaniach wsadowych) lub jeśli trwa to dłużej niż sekundę, nadal się nie powiedzie. Taka nieefektywność i zawodność jest nie do zniesienia w pracy zawodowej.
CodeManX
9
Prawdziwy problem polega na tym, że nigdy nie wiadomo, kiedy javascript zakończy ładowanie strony, a przeglądarka również o tym nie wie. Wyobraź sobie stronę, na której jakiś javascript ładuje coś z serwera w nieskończonej pętli. Z punktu widzenia przeglądarki - wykonywanie javascript nigdy się nie kończy, więc w jakim momencie chcesz, aby phantomjs powiedział ci, że się zakończyło? Ten problem jest nierozwiązywalny w przypadku ogólnym, z wyjątkiem oczekiwania na rozwiązanie limitu czasu i nadziei na najlepsze.
Maxim Galushka
5
Czy to nadal najlepsze rozwiązanie od 2016 roku? Wygląda na to, że powinniśmy być w stanie zrobić to lepiej.
Adam Thompson,
6
Jeśli masz kontrolę nad kodem, który próbujesz odczytać, możesz jawnie wywołać wywołanie phantom js: phantomjs.org/api/webpage/handler/on-callback.html
Andy Smith
52

Wolałbym okresowo sprawdzać document.readyStatestatus ( https://developer.mozilla.org/en-US/docs/Web/API/document.readyState ). Chociaż takie podejście jest trochę niezgrabne, możesz być pewien, że wewnątrz onPageReadyfunkcji używasz w pełni załadowanego dokumentu.

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

Dodatkowe wyjaśnienie:

Używanie zagnieżdżonych setTimeoutzamiast setIntervalzapobiegania checkReadyState"nakładaniu się" i warunków wyścigu, gdy jego wykonanie jest przedłużane z pewnych przypadkowych powodów. setTimeoutma domyślne opóźnienie 4 ms ( https://stackoverflow.com/a/3580085/1011156 ), więc aktywne odpytywanie nie wpłynie drastycznie na wydajność programu.

document.readyState === "complete"oznacza, że ​​dokument jest całkowicie załadowany wszystkimi zasobami ( https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness ).

Mateusz Charytoniuk
źródło
4
komentarz dotyczący setTimeout vs setInterval jest świetny.
Gal Bracha
1
readyStateuruchomi się tylko wtedy, gdy DOM zostanie w pełni załadowany, jednak wszelkie <iframe>elementy mogą nadal się ładować, więc tak naprawdę nie odpowiada na oryginalne pytanie
CodingIntrigue
1
@rgraham To nie jest idealne rozwiązanie, ale myślę, że możemy zrobić tyle tylko z tymi mechanizmami renderującymi. Będą skrajne przypadki, w których po prostu nie będziesz wiedział, czy coś jest w pełni załadowane. Pomyśl o stronie, na której zawartość jest celowo opóźniona o minutę lub dwie. Nierozsądne jest oczekiwanie, że proces renderowania będzie siedział i czekał przez nieokreślony czas. To samo dotyczy treści ładowanych ze źródeł zewnętrznych, które mogą działać wolno.
Brandon Elliott
3
Nie uwzględnia to żadnego ładowania JavaScript po pełnym załadowaniu DOM, na przykład w przypadku Backbone / Ember / Angular.
Adam Thompson
1
W ogóle mi nie wyszło. ReadyState complete mogło zostać uruchomione, ale w tym momencie strona była pusta.
Steve Staple
21

Możesz spróbować kombinacji przykładów waitfor i 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
źródło
3
Wygląda na to, że nie działałoby to ze stronami internetowymi, które używają którejkolwiek z technologii serwera push, ponieważ zasób będzie nadal używany po wystąpieniu onLoad.
nilfalse
Czy jakieś sterowniki np. poltergeist , masz taką funkcję?
Jared Beck
Czy można użyć waitFor do odpytywania całego tekstu HTML i wyszukania określonego słowa kluczowego? Próbowałem to zaimplementować, ale wygląda na to, że ankieta nie odświeża się do ostatnio pobranego źródła HTML.
fpdragon
14

Może możesz użyć wywołań zwrotnych onResourceRequestedionResourceReceived do wykrywania ładowania asynchronicznego. Oto przykład użycia tych wywołań zwrotnych z ich dokumentacji :

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

Możesz również spojrzeć na examples/netsniff.jsdziałający przykład.

Supr
źródło
Ale w tym przypadku nie mogę użyć jednego wystąpienia PhantomJS do załadowania więcej niż jednej strony naraz, prawda?
nilfalse
Czy onResourceRequested ma zastosowanie do żądań AJAX / międzydomenowych? Czy może dotyczy to tylko polubienia CSS, obrazów itp.?
CMCDragonkai
@CMCDragonkai Sam go nigdy nie używałem, ale na podstawie tego wygląda na to , że zawiera wszystkie żądania. Cytat:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
Niedziela,
Użyłem tej metody z renderowaniem PhantomJS na dużą skalę i działa całkiem dobrze. Potrzebujesz sprytu, aby śledzić żądania i obserwować, czy zawodzą lub przekroczą limit czasu. Więcej informacji: sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
Ryan Doherty
14

Oto rozwiązanie, które czeka na zakończenie wszystkich żądań zasobów. Po zakończeniu zarejestruje zawartość strony w konsoli i wygeneruje zrzut ekranu renderowanej strony.

Chociaż to rozwiązanie może służyć jako dobry punkt wyjścia, zauważyłem, że zawodzi, więc zdecydowanie nie jest to rozwiązanie kompletne!

Nie miałem szczęścia przy używaniu document.readyState.

Wpływ na mnie miał przykład waitfor.js, który można znaleźć na stronie przykładów 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
źródło
Dałem kciuki w górę, ale użyłem setTimeout z 10 zamiast interwału
GDmac
Powinieneś sprawdzić, czy parametr response.stage jest równy „end” przed usunięciem go z tablicy request, w przeciwnym razie może zostać usunięty przedwcześnie.
Reimund
To nie działa, jeśli twoja strona internetowa ładuje dynamicznie DOM
kolego
13

W moim programie używam logiki, aby ocenić, czy był on załadowany: obserwując żądanie sieciowe, jeśli nie było nowego żądania w ciągu ostatnich 200 ms, traktuję je jako załadowane.

Użyj tego po 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
źródło
11

Uważam, że to podejście jest przydatne w niektórych przypadkach:

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

Jeśli jesteś właścicielem strony, umieść w niej skrypt:

<script>
  window.onload = function(){
    console.log('page loaded');
  }
</script>
Brankodd
źródło
Wygląda to na naprawdę fajne obejście, jednak nie mogłem uzyskać żadnego komunikatu dziennika z mojej strony HTML / JavaScript, aby przejść przez phantomJS ... zdarzenie onConsoleMessage nigdy nie zostało wyzwolone, podczas gdy mogłem doskonale widzieć komunikaty w konsoli przeglądarki, i Nie mam pojęcia, dlaczego.
Dirk
1
Potrzebowałem page.onConsoleMessage = function (msg) {};
Andy Balaam
5

Znalazłem to rozwiązanie przydatne w aplikacji NodeJS. Używam go tylko w rozpaczliwych przypadkach, ponieważ uruchamia limit czasu, aby poczekać na pełne załadowanie strony.

Drugim argumentem jest funkcja zwrotna, która zostanie wywołana, gdy odpowiedź będzie gotowa.

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
źródło
3

To jest implementacja odpowiedzi Supr. Używa również setTimeout zamiast setInterval, jak sugerował Mateusz Charytoniuk.

Phantomjs zakończy działanie po 1000 ms, gdy nie będzie żadnego żądania ani odpowiedzi.

// 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
źródło
3

Oto kod, którego używam:

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

Zasadniczo, biorąc pod uwagę fakt, że powinieneś wiedzieć, że strona jest pobierana w całości, gdy dany element pojawia się w DOM. Więc skrypt będzie czekał, aż to się stanie.

Rocco Musolino
źródło
3

Używam personalnej mieszanki waitfor.jsprzykładu phantomjs .

To jest mój main.jsplik:

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

I lib/waitFor.jsplik (który jest po prostu kopiowaniem i wklejaniem waifFor()funkcji z waitfor.jsprzykładu 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
}

Ta metoda nie jest asynchroniczna, ale przynajmniej mam pewność, że wszystkie zasoby zostały załadowane, zanim spróbuję ich użyć.

Daishi
źródło
2

To stare pytanie, ale ponieważ szukałem pełnego załadowania strony, ale Spookyjs (który używa casperjs i phantomjs) i nie znalazłem swojego rozwiązania, stworzyłem własny skrypt, z tym samym podejściem, jakie uważa użytkownik. To podejście polega na tym, że przez określony czas, jeśli strona nie otrzymała lub nie rozpoczęła żadnego żądania, zakończy wykonywanie.

W pliku casper.js (jeśli zainstalowałeś go globalnie, ścieżka wyglądałaby tak, jak /usr/local/lib/node_modules/casperjs/modules/casper.js) dodaj następujące wiersze:

U góry pliku ze wszystkimi globalnymi zmiennymi:

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

Następnie wewnątrz funkcji "createPage (casper)" zaraz po "var page = require ('webpage'). Create ();" dodaj następujący kod:

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

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

Następnie wewnątrz „page.onResourceReceived = function onResourceReceived (resource) {” w pierwszym wierszu dodaj:

 resetTimeout()

Zrób to samo dla "page.onResourceRequested = function onResourceRequested (requestData, request) {"

Na koniec w „page.onLoadFinished = function onLoadFinished (status) {” w pierwszym wierszu dodaj:

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

I to wszystko, mam nadzieję, że ten pomoże komuś w tarapatach, tak jak ja. To rozwiązanie jest przeznaczone dla casperjs, ale działa bezpośrednio dla Spooky.

Powodzenia !

fdnieves
źródło
0

to jest moje rozwiązanie, które zadziałało dla mnie.

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);
    }
});
Tomek
źródło