Phantomjs 不等待“完整”页面加载

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

问题在于许多网站都在异步加载次要内容,这就是为什么 Phantom 的 onLoadFinish 回调(类似于 HTML 中的 onLoad)在并非所有内容都已加载的情况下过早启动的原因。有人能建议我如何等待一个完整的网页负载,以使,例如,一个截图所有动态内容,如广告?

155419 次浏览

也许您可以使用 ABC0和 onResourceReceived回调来检测异步加载:

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以获得一个工作示例。

您可以尝试结合使用 wait 和栅格化示例:

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

另一种方法是让 PhantomJS 在页面加载完成之后再进行渲染,就像常规的 栅格化示例那样,但是要有更长的超时时间,以允许 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
}
});

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

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

我宁愿定期检查 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的默认延迟为4ms (https://stackoverflow.com/a/3580085/1011156) ,因此主动轮询不会严重影响程序性能。

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

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

我仍然认为以上的剪辑是一个很好的起点,可能在大多数情况下工作,但也可能需要创建一个具体的解决方案,以处理特定的网站。

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

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

这是一个老问题,但是因为我在寻找完整的页面加载,但是 Spookyjs (使用 casperjs 和 phantomjs)没有找到我的解决方案,所以我用与 deemstone 用户相同的方法编写了自己的脚本。 这种方法的作用是,在给定的时间内,如果页面没有接收或启动任何请求,它将结束执行。

在 casper.js 文件(如果在全局范围内安装,路径应该是/usr/local/lib/node _ module/casperjs/module/casper.js)中添加以下代码行:

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

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

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

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


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

然后在第一行的“ page.onResourceRecected = function onResourceRecected (resource){”中添加:

 resetTimeout()

对“ page.onResourceRequsted = function onResourceRequsted (requestData,request){”执行同样的操作

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

 if(!reqResFinished)
{
return
}
reqResFinished = false

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

祝你好运!

这是 Supr 的答案的一个实现,而且它使用 setTimeout 而不是 Mateusz Charytoniuk 建议的 setInterval。

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

// 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 上时,您应该知道页面已经完全下载了。所以脚本会等到这一切发生。

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

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

我用 document.readyState的时候运气不太好。

我受到了 Phantomjs 示例页上的 等等 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);
});

我使用个人混合的幻影 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
}

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

这是我的解决方案,对我很有效。

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

更新

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

告诉它移动鼠标,直到网址改变为主页给浏览器足够的时间,因为它需要改变。然后告诉它等待页面完成加载,允许页面在内容被抓取之前完全加载。

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