利用NodeJS和PhantomJS抓取网站页面信息以及网站截图
安装PhantomJS
首先,去PhantomJS官网下载对应平台的版本,或者下载源代码自行编译。然后将PhantomJS配置进环境变量,输入
$phantomjs
如果有反应,那么就可以进行下一步了。
利用PhantomJS进行简单截图
这里我们设置了窗口大小为1024*800:
page.viewportSize={width:1024,height:800};
截取从(0,0)为起点的1024*800大小的图像:
禁止Javascript,允许图片载入,并将userAgent改为"Mozilla/5.0(WindowsNT6.1)AppleWebKit/537.31(KHTML,likeGecko)PhantomJS/19.0":
然后利用page.open打开页面,最后截图输出到./snapshot/test.png中:
NodeJS与PhantomJS通讯
我们先来看看PhantomJS能做什么通讯。
命令行传参例如:
phantomjssnapshot.jshttp://www.baidu.com
命令行传参只能在PhantomJS开启时进行传参,在运行过程中就无能为力了。
标准输出标准输出能从PhantomJS向NodeJS输出数据,但却没法从NodeJS传数据给PhantomJS。
不过测试中,标准输出是这几种方式传输最快的,在大量数据传输中应当考虑。
HTTPPhantomJS向NodeJS服务发出HTTP请求,然后NodeJS返回相应的数据。
这种方式很简单,但是请求只能由PhantomJS发出。
Websocket值得注意的是PhantomJS1.9.0支持Websocket了,不过可惜是hixie-76Websocket,不过毕竟还是提供了一种NodeJS主动向PhantomJS通讯的方案了。
测试中,我们发现PhantomJS连上本地的Websocket服务居然需要1秒左右,暂时不考虑这种方法吧。
phantomjs-nodephantomjs-node成功将PhantomJS作为NodeJS的一个模块来使用,但我们看看作者的原理解释:
Iwillanswerthatquestionwithaquestion.Howdoyoucommunicatewithaprocessthatdoesn"tsupportsharedmemory,sockets,FIFOs,orstandardinput?
Well,there"sonethingPhantomJSdoessupport,andthat"sopeningwebpages.Infact,it"sreallygoodatopeningwebpages.SowecommunicatewithPhantomJSbyspinningupaninstanceofExpressJS,openingPhantominasubprocess,andpointingitataspecialwebpagethatturnssocket.iomessagesinto alert()
calls.Those alert()
callsarepickedupbyPhantomandthereyougo!
ThecommunicationitselfhappensviaJamesHalliday"sfantastic dnode library,whichfortunatelyworkswellenoughwhencombinedwith browserify torunstraightoutofPhantomJS"spidginJavascriptenvironment.
实际上phantomjs-node使用的也是HTTP或者Websocket来进行通讯,不过其依赖庞大,我们只想做一个简单的东西,暂时还是不考虑这个东东吧。
设计图
让我们开始吧
我们在第一版中选用HTTP进行实现。
首先利用cluster进行简单的进程守护(index.js):
if(!fs.existsSync("./snapshot")){ if(cluster.isMaster){ cluster.on("exit",function(worker){ 然后利用connect做我们的对外API(extract.js): varapp=connect() varcampaignId=req.body.campaignId function_deal(id,url,imagePath){ jobMan.register(campaignId,urls,req,res,next); }) })(); 这里我们引用了两个模块bridge和jobMan。 其中bridge是HTTP通讯桥梁,jobMan是工作管理器。我们通过campaignId来对应一个job,然后将job和response委托给jobMan管理。然后启动PhantomJS进行处理。 通讯桥梁负责接受或者返回job的相关信息,并交给jobMan(bridge.js): returnfunction(req,res,next){ })(); 如果requestmethod为POST,则我们认为PhantomJS正在给我们推送job的相关信息。而为GET时,则认为其要获取job的信息。 jobMan负责管理job,并发送目前得到的job信息通过response返回给client(jobMan.js): function_send(campaignId){ functionwatch(campaignId,req,res,next){ functionfire(opts){ if(job){ if(!job.waiting){ functiongetUrls(campaignId){ return{ })(); 这里我们用到fetch对html进行抓取其title和description,fetch实现比较简单(fetch.js): returnfunction(html){ vartitle=html.match(/\<title\>(.*?)\<\/title\>/) if(meta){ (title&&title[1]!=="")?(title=title[1]):(title="NoTitle"); return{ })(); 最后是PhantomJS运行的源代码,其启动后通过HTTP向bridge获取job信息,然后每完成job的其中一个url就通过HTTP返回给bridge(snapshot.js): functionsnapshot(id,url,imagePath){ varpostMan={ this.len=urls.length; if(this.len){
module.exports=(function(){
"usestrict"
varcluster=require("cluster")
,fs=require("fs");
fs.mkdirSync("./snapshot");
}
cluster.fork();
console.log("Worker"+worker.id+"died:(");
process.nextTick(function(){
cluster.fork();
});
})
}else{
require("./extract.js");
}
})();
module.exports=(function(){
"usestrict"
varconnect=require("connect")
,fs=require("fs")
,spawn=require("child_process").spawn
,jobMan=require("./lib/jobMan.js")
,bridge=require("./lib/bridge.js")
,pkg=JSON.parse(fs.readFileSync("./package.json"));
.use(connect.logger("dev"))
.use("/snapshot",connect.static(__dirname+"/snapshot",{maxAge:pkg.maxAge}))
.use(connect.bodyParser())
.use("/bridge",bridge)
.use("/api",function(req,res,next){
if(req.method!=="POST"||!req.body.campaignId)returnnext();
if(!req.body.urls||!req.body.urls.length)returnjobMan.watch(req.body.campaignId,req,res,next);
,imagesPath="./snapshot/"+campaignId+"/"
,urls=[]
,url
,imagePath;
//justpushintourlslist
urls.push({
id:id,
url:url,
imagePath:imagePath
});
}
for(vari=req.body.urls.length;i--;){
url=req.body.urls[i];
imagePath=imagesPath+i+".png";
_deal(i,url,imagePath);
}
varsnapshot=spawn("phantomjs",["snapshot.js",campaignId]);
snapshot.stdout.on("data",function(data){
console.log("stdout:"+data);
});
snapshot.stderr.on("data",function(data){
console.log("stderr:"+data);
});
snapshot.on("close",function(code){
console.log("snapshotexitedwithcode"+code);
});
.use(connect.static(__dirname+"/html",{maxAge:pkg.maxAge}))
.listen(pkg.port,function(){console.log("listen:"+"http://localhost:"+pkg.port);});
module.exports=(function(){
"usestrict"
varjobMan=require("./jobMan.js")
,fs=require("fs")
,pkg=JSON.parse(fs.readFileSync("./package.json"));
if(req.headers.secret!==pkg.secret)returnnext();
//SnapshotAPPcanposturlinformation
if(req.method==="POST"){
varbody=JSON.parse(JSON.stringify(req.body));
jobMan.fire(body);
res.end("");
//SnapshotAPPcangettheurlsshouldextract
}else{
varurls=jobMan.getUrls(req.url.match(/campaignId=([^&]*)(\s|&|$)/)[1]);
res.writeHead(200,{"Content-Type":"application/json"});
res.statuCode=200;
res.end(JSON.stringify({urls:urls}));
}
};
module.exports=(function(){
"usestrict"
varfs=require("fs")
,fetch=require("./fetch.js")
,_jobs={};
varjob=_jobs[campaignId];
if(!job)return;
if(job.waiting){
job.waiting=false;
clearTimeout(job.timeout);
varfinished=(job.urlsNum===job.finishNum)
,data={
campaignId:campaignId,
urls:job.urls,
finished:finished
};
job.urls=[];
varres=job.res;
if(finished){
_jobs[campaignId]=null;
delete_jobs[campaignId]
}
res.writeHead(200,{"Content-Type":"application/json"});
res.statuCode=200;
res.end(JSON.stringify(data));
}
}
functionregister(campaignId,urls,req,res,next){
_jobs[campaignId]={
urlsNum:urls.length,
finishNum:0,
urls:[],
cacheUrls:urls,
res:null,
waiting:false,
timeout:null
};
watch(campaignId,req,res,next);
}
_jobs[campaignId].res=res;
//20stimeout
_jobs[campaignId].timeout=setTimeout(function(){
_send(campaignId);
},20000);
}
varcampaignId=opts.campaignId
,job=_jobs[campaignId]
,fetchObj=fetch(opts.html);
if(+opts.status&&fetchObj.title){
job.urls.push({
id:opts.id,
url:opts.url,
image:opts.image,
title:fetchObj.title,
description:fetchObj.description,
status:+opts.status
});
}else{
job.urls.push({
id:opts.id,
url:opts.url,
status:+opts.status
});
}
job.waiting=true;
setTimeout(function(){
_send(campaignId);
},500);
}
job.finishNum++;
}else{
console.log("jobcannotfound!");
}
}
varjob=_jobs[campaignId];
if(job)returnjob.cacheUrls;
}
register:register,
watch:watch,
fire:fire,
getUrls:getUrls
};
module.exports=(function(){
"usestrict"
if(!html)return{title:false,description:false};
,meta=html.match(/\<meta\s(.*?)\/?\>/g)
,description;
for(vari=meta.length;i--;){
if(meta[i].indexOf("name="description"")>-1||meta[i].indexOf("name="Description"")>-1){
description=meta[i].match(/content\=\"(.*?)\"/)[1];
}
}
}
description||(description="NoDescription");
title:title,
description:description
};
};
varwebpage=require("webpage")
,args=require("system").args
,fs=require("fs")
,campaignId=args[1]
,pkg=JSON.parse(fs.read("./package.json"));
varpage=webpage.create()
,send
,begin
,save
,end;
page.viewportSize={width:1024,height:800};
page.clipRect={top:0,left:0,width:1024,height:800};
page.settings={
javascriptEnabled:false,
loadImages:true,
userAgent:"Mozilla/5.0(WindowsNT6.1)AppleWebKit/537.31(KHTML,likeGecko)PhantomJS/1.9.0"
};
page.open(url,function(status){
vardata;
if(status==="fail"){
data=[
"campaignId=",
campaignId,
"&url=",
encodeURIComponent(url),
"&id=",
id,
"&status=",
].join("");
postPage.open("http://localhost:"+pkg.port+"/bridge","POST",data,function(){});
}else{
page.render(imagePath);
varhtml=page.content;
//callbackNodeJS
data=[
"campaignId=",
campaignId,
"&html=",
encodeURIComponent(html),
"&url=",
encodeURIComponent(url),
"&image=",
encodeURIComponent(imagePath),
"&id=",
id,
"&status=",
].join("");
postMan.post(data);
}
//releasethememory
page.close();
});
}
postPage:null,
posting:false,
datas:[],
len:0,
currentNum:0,
init:function(snapshot){
varpostPage=webpage.create();
postPage.customHeaders={
"secret":pkg.secret
};
postPage.open("http://localhost:"+pkg.port+"/bridge?campaignId="+campaignId,function(){
varurls=JSON.parse(postPage.plainText).urls
,url;
for(vari=this.len;i--;){
url=urls[i];
snapshot(url.id,url.url,url.imagePath);
}
}
});
this.postPage=postPage;
},
post:function(data){
this.datas.push(data);
if(!this.posting){
this.posting=true;
this.fire();
}
},
fire:function(){
if(this.datas.length){
vardata=this.datas.shift()
,that=this;
this.postPage.open("http://localhost:"+pkg.port+"/bridge","POST",data,function(){
that.fire();
//killchildprocess
setTimeout(function(){
if(++this.currentNum===this.len){
that.postPage.close();
phantom.exit();
}
},500);
});
}else{
this.posting=false;
}
}
};
postMan.init(snapshot);相关文章