幻影不等待“完整”页面加载

phantomjs not waiting for "full" page load

本文关键字:加载 完整 等待 幻影      更新时间:2023-09-26

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

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

另一种方法是要求PhantomJS在页面加载后等待一段时间,然后再进行渲染,如常规光栅化.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 
    }
});

我宁愿定期检查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)。

编辑 2022:我在 8 年前创建了这个响应,从那时起我就没有使用 PhantomJS。在某些情况下,它现在很可能不起作用。另外,现在我认为不可能创建一个一刀切的解决方案来绝对确保页面已加载。这是因为某些页面可能会在文档准备就绪后加载其他资源。例如,网站上可能有一些JS代码等待文档准备就绪,然后加载一些其他资产(在文档状态更改为ready之后) - 在这种情况下,onPageReady将触发,之后页面将再次开始加载更多资源。

我仍然认为上述截图是一个很好的起点,在大多数情况下可能有效,但也可能需要创建特定的解决方案来处理特定的网站。

您可以尝试将等待和栅格化示例组合:

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

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

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

我使用document.readyState没有太多运气.

我受到 phantomjs 示例页面上的 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);
});

也许您可以使用onResourceRequestedonResourceReceived回调来检测异步加载。下面是使用其文档中的这些回调的示例:

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以获取工作示例。

在我的程序中,我使用一些逻辑来判断它是否在加载:观察它的网络请求,如果过去 200 毫秒没有新请求,我将其处理在负载上。

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

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

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

比如果你拥有页面,把一些脚本放进去:

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

我发现这个解决方案在 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);

这是Supr答案的实现。此外,它使用 setTimeout 而不是 Mateusz Charytoniuk 建议的 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();
这是我

使用的代码:

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上时,页面已完全下载。因此,脚本将等到发生这种情况。

我使用幻影waitfor.js个人混合的例子。

这是我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示例中复制和粘贴waifFor()函数):

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
}

此方法不是异步的,但至少我可以确保在尝试使用它们之前已加载所有资源。

这是一个老问题,但由于我正在寻找整页加载,但对于 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() {}

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

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

然后在第一行的"page.onResourceReceived = function onResourceReceived(resource) {"中添加:

 resetTimeout()

对"page.onResourceRequest= function onResourceRequest(requestData, request) {"执行相同的操作

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

 if(!reqResFinished)
 {
      return
 }
 reqResFinished = false

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

祝你好运!

这是我

的解决方案,它对我有用.

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

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

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