【问题标题】:phantomjs not waiting for "full" page loadphantomjs 不等待“完整”页面加载
【发布时间】:2012-07-05 14:30:02
【问题描述】:

我正在使用PhantomJS v1.4.1 加载一些网页。我无法访问他们的服务器端,我只是获得指向他们的链接。我正在使用过时版本的 Phantom,因为我需要在该网页上支持 Adob​​e Flash。

问题是许多网站正在异步加载他们的次要内容,这就是为什么 Phantom 的 onLoadFinished 回调(类似于 HTML 中的 onLoad)在并非所有内容仍然加载时过早触发的原因。任何人都可以建议我如何等待网页完全加载,例如,制作包含所有动态内容(如广告)的屏幕截图?

【问题讨论】:

  • 我认为是时候接受答案了

标签: javascript events phantomjs


【解决方案1】:

也许您可以使用onResourceRequested and onResourceReceived callbacks 来检测异步加载。这是使用这些回调的示例from their documentation

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

此外,您还可以查看 examples/netsniff.js 的工作示例。

【讨论】:

  • 但在这种情况下,我不能使用一个 PhantomJS 实例一次加载多个页面,对吧?
  • onResourceRequested 是否适用于 AJAX/跨域请求?还是仅适用于 CSS、图像.. 等?
  • @CMCDragonkai 我自己从未使用过它,但基于this 似乎它包括所有请求。引用:All the resource requests and responses can be sniffed using onResourceRequested and onResourceReceived
  • 我已经在大规模 PhantomJS 渲染中使用过这种方法,效果很好。您确实需要很多智能来跟踪请求并观察它们是否失败或超时。更多信息:sorcery.smugmug.com/2013/12/17/using-phantomjs-at-scale
【解决方案2】:

您可以尝试结合使用 waitfor 和 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);
        }
    });
}

【讨论】:

  • 似乎它不适用于使用任何服务器推送技术的网页,因为在 onLoad 发生后资源仍将被使用。
  • 做任何驱动程序,例如。 poltergeist,有这样的功能吗?
  • 是否可以使用 waitFor 来轮询整个 html 文本并搜索已定义的关键字?我试图实现这一点,但轮询似乎没有刷新到最新下载的 html 源。
【解决方案3】:

另一种方法是要求 PhantomJS 在页面加载后等待一段时间再进行渲染,就像常规的 rasterize.js 示例一样,但需要更长的超时时间以允许 JavaScript 完成加载额外资源:

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

【讨论】:

  • 是的,目前我坚持这种方法。
  • 这是一个糟糕的解决方案,抱歉(这是 PhantomJS 的错!)。如果您等待整整一秒钟,但加载需要 20 毫秒,这完全是浪费时间(想想批处理作业),或者如果花费的时间超过一秒钟,它仍然会失败。这样的低效和不可靠对于专业工作来说是无法忍受的。
  • 这里真正的问题是你永远不知道javascript什么时候会完成页面加载,而浏览器也不知道。想象一下有一些 javascript 在无限循环中从服务器加载某些内容的站点。从浏览器的角度来看 - javascript 执行永无止境,那么你想让 phantomjs 告诉你它已经完成的那一刻是什么时候?这个问题在一般情况下是无法解决的,除非等待超时解决方案并希望最好。
  • 这仍然是 2016 年的最佳解决方案吗?看来我们应该可以做得比这更好。
  • 如果你可以控制你试图阅读的代码,你可以显式调用幻像js回调:phantomjs.org/api/webpage/handler/on-callback.html
【解决方案4】:

在我的程序中,我使用一些逻辑来判断它是否是onload:看它的网络请求,如果过去200ms没有新的请求,我就认为它是onload。

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

【讨论】:

    【解决方案5】:

    我宁愿定期检查 document.readyState 状态 (https://developer.mozilla.org/en-US/docs/Web/API/document.readyState)。虽然这种方法有点笨拙,但您可以确定在 onPageReady 函数中您使用的是完全加载的文档。

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

    补充说明:

    使用嵌套的setTimeout 而不是setInterval 可以防止checkReadyState 在由于某些随机原因延长其执行时间时出现“重叠”和竞争条件。 setTimeout 的默认延迟为 4 毫秒 (https://stackoverflow.com/a/3580085/1011156),因此主动轮询不会显着影响程序性能。

    document.readyState === "complete" 表示文档已完全加载所有资源 (https://html.spec.whatwg.org/multipage/dom.html#current-document-readiness)。

    【讨论】:

    • setTimeout vs setInterval 的评论很棒。
    • readyState 只会在 DOM 完全加载后触发,但是任何 &lt;iframe&gt; 元素可能仍在加载,因此它并不能真正回答原始问题
    • @rgraham 这并不理想,但我认为我们只能用这些渲染器做这么多。在某些极端情况下,您只是不知道某些东西是否已完全加载。想想一个内容被故意延迟一两分钟的页面。期望渲染进程坐下来等待不确定的时间是不合理的。从外部来源加载的内容也可能很慢。
    • 这不会考虑在 DOM 完全加载后加载任何 JavaScript,例如 Backbone/Ember/Angular。
    • 对我来说根本不起作用。 readyState complete 可能已经触发,但此时页面是空白的。
    【解决方案6】:

    我发现这种方法在某些情况下很有用:

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

    如果您拥有该页面,请在其中放置一些脚本:

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

    【讨论】:

    • 这看起来是一个非常好的解决方法,但是,我无法从我的 HTML/JavaScript 页面获取任何日志消息以通过 phantomJS ... onConsoleMessage 事件从未触发,而我可以看到浏览器控制台上的消息完美,我不知道为什么。
    • 我需要 page.onConsoleMessage = function(msg){};
    【解决方案7】:

    我发现这个解决方案在 NodeJS 应用程序中很有用。 我只是在绝望的情况下使用它,因为它会启动超时以等待整个页面加载。

    第二个参数是回调函数,一旦响应准备好就会被调用。

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

    【讨论】:

      【解决方案8】:

      这是一个老问题,但由于我一直在寻找完整页面加载,但对于 Spookyjs(使用 casperjs 和 phantomjs)并且没有找到我的解决方案,我为此制作了自己的脚本,方法与用户认为。 这种方法的作用是,在给定的时间内,如果页面没有收到或启动任何请求,它将结束执行。

      在 casper.js 文件上(如果您全局安装,路径类似于 /usr/local/lib/node_modules/casperjs/modules/casper.js)添加以下行:

      在所有全局变量的文件顶部:

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

      然后在“var page = require('webpage').create();”之后的函数“createPage(casper)”中添加以下代码:

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

      然后在“page.onResourceReceived = function onResourceReceived(resource) {”里面第一行添加:

       resetTimeout()
      

      对“page.onResourceRequested = function onResourceRequested(requestData, request) {”做同样的事情

      最后,在“page.onLoadFinished = function onLoadFinished(status) {”的第一行添加:

       if(!reqResFinished)
       {
            return
       }
       reqResFinished = false
      

      就是这样,希望这个可以帮助像我一样遇到麻烦的人。此解决方案适用于 casperjs,但直接适用于 Spooky。

      祝你好运!

      【讨论】:

        【解决方案9】:

        这是 Supr 答案的实现。它还像 Mateusz Charytoniuk 建议的那样使用 setTimeout 而不是 setInterval。

        当没有任何请求或响应时,Phantomjs 将在 1000 毫秒内退出。

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

        【讨论】:

          【解决方案10】:

          这是我使用的代码:

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

          基本上,当给定元素出现在 DOM 上时,您应该知道页面已完全下载。所以脚本会一直等到发生这种情况。

          【讨论】:

            【解决方案11】:

            这是一个等待所有资源请求完成的解决方案。完成后,它将页面内容记录到控制台并生成渲染页面的屏幕截图。

            虽然这个解决方案可以作为一个很好的起点,但我观察到它失败了,所以它绝对不是一个完整的解决方案!

            我在使用document.readyState 时运气不佳。

            我受到phantomjs examples page 上的waitfor.js 示例的影响。

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

            【讨论】:

            • 点赞,但使用 setTimeout 和 10,而不是间隔
            • 在从 requests 数组中删除之前,您应该检查 response.stage 是否等于 'end',否则可能会过早删除。
            • 如果您的网页动态加载 DOM,这将不起作用
            【解决方案12】:

            我使用 phantomjs waitfor.js example 的个人混合。

            这是我的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);
              }
            });
            

            还有lib/waitFor.js 文件(它只是来自phantomjs waitfor.js examplewaifFor() 函数的复制和粘贴):

            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
            }
            

            这个方法不是异步的,但至少我确信在我尝试使用它们之前已经加载了所有资源。

            【讨论】:

              【解决方案13】:

              这是我的解决方案,它对我有用。

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

              【讨论】:

                【解决方案14】:

                在页面加载时移动鼠标应该可以工作。

                 page.sendEvent('click',200, 660);
                
                do { phantom.page.sendEvent('mousemove'); } while (page.loading);
                

                更新

                提交表单时,没有返回任何内容,因此程序停止了。程序没有等待页面加载,因为重定向开始需要几秒钟。

                告诉它移动鼠标直到主页的 URL 更改为浏览器所需的更改时间。然后告诉它等待页面完成加载允许页面在抓取内容之前完全加载。

                page.evaluate(function () {
                document.getElementsByClassName('btn btn-primary btn-block')[0].click();
                });
                do { phantom.page.sendEvent('mousemove'); } while (page.evaluate(function()
                {
                return document.location != "https://www.bestwaywholesale.co.uk/";
                }));
                do { phantom.page.sendEvent('mousemove'); } while (page.loading);
                

                【讨论】:

                • 恐怕这并没有多大帮助,但感谢您的帮助:)
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2019-02-10
                • 1970-01-01
                • 2023-03-08
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多