一次使用NodeJS实现网页爬虫记
前言
几个月之前,有同事找我要PHP CI框架写的OA系统。他跟我说,他需要学习PHP CI框架,我建议他学习大牛写的国产优秀框架QeePHP。
我上QeePHP官网,发现官方网站打不开了,GOOGLE了一番,发现QeePHP框架已经没人维护了。API文档资料都没有了,那可怎么办?
毕竟QeePHP学习成本挺高的。GOOGLE时,我发现已经有人把文档整理好,放在自己的个人网站上了。我在想:万一放文档的个人站点也挂了,
怎么办?还是保存到自己的电脑上比较保险。于是就想着用NodeJS写个爬虫抓取需要的文档到本地。后来抓取完成之后,干脆写了一个通用版本的,
可以抓取任意网站的内容。
爬虫原理
抓取初始URL的页面内容,提取URL列表,放入URL队列中,
从URL队列中取一个URL地址,抓取这个URL地址的内容,提取URL列表,放入URL队列中
。。。。。。
。。。。。。

NodeJS实现源码
/**
* @desc 网页爬虫 抓取某个站点
*
* @todolist
* URL队列很大时处理
* 302跳转
* 处理COOKIE
* iconv-lite解决乱码
* 大文件偶尔异常退出
*
* @author WadeYu
* @date 2015-05-28
* @copyright by WadeYu
* @version 0.0.1
*/
/**
* @desc 依赖的模块
*/
var fs = require("fs");
var http = require("http");
var https = require("https");
var urlUtil = require("url");
var pathUtil = require("path");
/**
* @desc URL功能类
*/
var Url = function(){};
/**
* @desc 修正被访问地址分析出来的URL 返回合法完整的URL地址
*
* @param string url 访问地址
* @param string url2 被访问地址分析出来的URL
*
* @return string || boolean
*/
Url.prototype.fix = function(url,url2){
if(!url || !url2){
return false;
}
var oUrl = urlUtil.parse(url);
if(!oUrl["protocol"] || !oUrl["host"] || !oUrl["pathname"]){//无效的访问地址
return false;
}
if(url2.substring(0,2) === "//"){
url2 = oUrl["protocol"]+url2;
}
var oUrl2 = urlUtil.parse(url2);
if(oUrl2["host"]){
if(oUrl2["hash"]){
delete oUrl2["hash"];
}
return urlUtil.format(oUrl2);
}
var pathname = oUrl["pathname"];
if(pathname.indexOf('/') > -1){
pathname = pathname.substring(0,pathname.lastIndexOf('/'));
}
if(url2.charAt(0) === '/'){
pathname = '';
}
url2 = pathUtil.normalize(url2); //修正 ./ 和 ../
url2 = url2.replace(/\\/g,'/');
while(url2.indexOf("../") > -1){ //修正以../开头的路径
pathname = pathUtil.dirname(pathname);
url2 = url2.substring(3);
}
if(url2.indexOf('#') > -1){
url2 = url2.substring(0,url2.lastIndexOf('#'));
} else if(url2.indexOf('?') > -1){
url2 = url2.substring(0,url2.lastIndexOf('?'));
}
var oTmp = {
"protocol": oUrl["protocol"],
"host": oUrl["host"],
"pathname": pathname + '/' + url2,
};
return urlUtil.format(oTmp);
};
/**
* @desc 判断是否是合法的URL地址一部分
*
* @param string urlPart
*
* @return boolean
*/
Url.prototype.isValidPart = function(urlPart){
if(!urlPart){
return false;
}
if(urlPart.indexOf("javascript") > -1){
return false;
}
if(urlPart.indexOf("mailto") > -1){
return false;
}
if(urlPart.charAt(0) === '#'){
return false;
}
if(urlPart === '/'){
return false;
}
if(urlPart.substring(0,4) === "data"){//base64编码图片
return false;
}
return true;
};
/**
* @desc 获取URL地址 路径部分 不包含域名以及QUERYSTRING
*
* @param string url
*
* @return string
*/
Url.prototype.getUrlPath = function(url){
if(!url){
return '';
}
var oUrl = urlUtil.parse(url);
if(oUrl["pathname"] && (/\/$/).test(oUrl["pathname"])){
oUrl["pathname"] += "index.html";
}
if(oUrl["pathname"]){
return oUrl["pathname"].replace(/^\/+/,'');
}
return '';
};
/**
* @desc 文件内容操作类
*/
var File = function(obj){
var obj = obj || {};
this.saveDir = obj["saveDir"] ? obj["saveDir"] : ''; //文件保存目录
};
/**
* @desc 内容存文件
*
* @param string filename 文件名
* @param mixed content 内容
* @param string charset 内容编码
* @param Function cb 异步回调函数
* @param boolean bAppend
*
* @return boolean
*/
File.prototype.save = function(filename,content,charset,cb,bAppend){
if(!content || !filename){
return false;
}
var filename = this.fixFileName(filename);
if(typeof cb !== "function"){
var cb = function(err){
if(err){
console.log("内容保存失败 FILE:"+filename);
}
};
}
var sSaveDir = pathUtil.dirname(filename);
var self = this;
var cbFs = function(){
var buffer = new Buffer(content,charset ? charset : "utf8");
fs.open(filename, bAppend ? 'a' : 'w', 0666, function(err,fd){
if (err){
cb(err);
return ;
}
var cb2 = function(err){
cb(err);
fs.close(fd);
};
fs.write(fd,buffer,0,buffer.length,0,cb2);
});
};
fs.exists(sSaveDir,function(exists){
if(!exists){
self.mkdir(sSaveDir,"0666",function(){
cbFs();
});
} else {
cbFs();
}
});
};
/**
* @desc 修正保存文件路径
*
* @param string filename 文件名
*
* @return string 返回完整的保存路径 包含文件名
*/
File.prototype.fixFileName = function(filename){
if(pathUtil.isAbsolute(filename)){
return filename;
}
if(this.saveDir){
this.saveDir = this.saveDir.replace(/[\\/]$/,pathUtil.sep);
}
return this.saveDir + pathUtil.sep + filename;
};
/**
* @递归创建目录
*
* @param string 目录路径
* @param mode 权限设置
* @param function 回调函数
* @param string 父目录路径
*
* @return void
*/
File.prototype.mkdir = function(sPath,mode,fn,prefix){
sPath = sPath.replace(/\\+/g,'/');
var aPath = sPath.split('/');
var prefix = prefix || '';
var sPath = prefix + aPath.shift();
var self = this;
var cb = function(){
fs.mkdir(sPath,mode,function(err){
if ( (!err) || ( ([47,-4075]).indexOf(err["errno"]) > -1 ) ){ //创建成功或者目录已存在
if (aPath.length > 0){
self.mkdir( aPath.join('/'),mode,fn, sPath.replace(/\/$/,'')+'/' );
} else {
fn();
}
} else {
console.log(err);
console.log('创建目录:'+sPath+'失败');
}
});
};
fs.exists(sPath,function(exists){
if(!exists){
cb();
} else if(aPath.length > 0){
self.mkdir(aPath.join('/'),mode,fn, sPath.replace(/\/$/,'')+'/' );
} else{
fn();
}
});
};
/**
* @递归删除目录 待完善 异步不好整
*
* @param string 目录路径
* @param function 回调函数
*
* @return void
*/
File.prototype.rmdir = function(path,fn){
var self = this;
fs.readdir(path,function(err,files){
if(err){
if(err.errno == -4052){ //不是目录
fs.unlink(path,function(err){
if(!err){
fn(path);
}
});
}
} else if(files.length === 0){
fs.rmdir(path,function(err){
if(!err){
fn(path);
}
});
}else {
for(var i = 0; i < files.length; i++){
self.rmdir(path+'/'+files[i],fn);
}
}
});
};
/**
* @desc 简单日期对象
*/
var oDate = {
time:function(){//返回时间戳 毫秒
return (new Date()).getTime();
},
date:function(fmt){//返回对应格式日期
var oDate = new Date();
var year = oDate.getFullYear();
var fixZero = function(num){
return num < 10 ? ('0'+num) : num;
};
var oTmp = {
Y: year,
y: (year+'').substring(2,4),
m: fixZero(oDate.getMonth()+1),
d: fixZero(oDate.getDate()),
H: fixZero(oDate.getHours()),
i: fixZero(oDate.getMinutes()),
s: fixZero(oDate.getSeconds()),
};
for(var p in oTmp){
if(oTmp.hasOwnProperty(p)){
fmt = fmt.replace(p,oTmp[p]);
}
}
return fmt;
},
};
/**
* @desc 未抓取过的URL队列
*/
var aNewUrlQueue = [];
/**
* @desc 已抓取过的URL队列
*/
var aGotUrlQueue = [];
/**
* @desc 统计
*/
var oCnt = {
total:0,//抓取总数
succ:0,//抓取成功数
fSucc:0,//文件保存成功数
};
/**
* 可能有问题的路径的长度 超过打监控日志
*/
var sPathMaxSize = 120;
/**
* @desc 爬虫类
*/
var Robot = function(obj){
var obj = obj || {};
//所在域名
this.domain = obj.domain || '';
//抓取开始的第一个URL
this.firstUrl = obj.firstUrl || '';
//唯一标识
this.id = this.constructor.incr();
//内容落地保存路径
this.saveDir = obj.saveDir || '';
//是否开启调试功能
this.debug = obj.debug || false;
//第一个URL地址入未抓取队列
if(this.firstUrl){
aNewUrlQueue.push(this.firstUrl);
}
//辅助对象
this.oUrl = new Url();
this.oFile = new File({saveDir:this.saveDir});
};
/**
* @desc 爬虫类私有方法---返回唯一爬虫编号
*
* @return int
*/
Robot.id = 1;
Robot.incr = function(){
return this.id++;
};
/**
* @desc 爬虫开始抓取
*
* @return boolean
*/
Robot.prototype.crawl = function(){
if(aNewUrlQueue.length > 0){
var url = aNewUrlQueue.pop();
this.sendReq(url);
oCnt.total++;
aGotUrlQueue.push(url);
} else {
if(this.debug){
console.log("抓取结束");
console.log(oCnt);
}
}
return true;
};
/**
* @desc 发起HTTP请求
*
* @param string url URL地址
*
* @return boolean
*/
Robot.prototype.sendReq = function(url){
var req = '';
if(url.indexOf("https") > -1){
req = https.request(url);
} else {
req = http.request(url);
}
var self = this;
req.on('response',function(res){
var aType = self.getResourceType(res.headers["content-type"]);
var data = '';
if(aType[2] !== "binary"){
//res.setEncoding(aType[2] ? aType[2] : "utf8");//非支持的内置编码会报错
} else {
res.setEncoding("binary");
}
res.on('data',function(chunk){
data += chunk;
});
res.on('end',function(){ //获取数据结束
self.debug && console.log("抓取URL:"+url+"成功\n");
self.handlerSuccess(data,aType,url);
data = null;
});
res.on('error',function(){
self.handlerFailure();
self.debug && console.log("服务器端响应失败URL:"+url+"\n");
});
}).on('error',function(err){
self.handlerFailure();
self.debug && console.log("抓取URL:"+url+"失败\n");
}).on('finish',function(){//调用END方法之后触发
self.debug && console.log("开始抓取URL:"+url+"\n");
});
req.end();//发起请求
};
/**
* @desc 提取HTML内容里的URL
*
* @param string html HTML文本
*
* @return []
*/
Robot.prototype.parseUrl = function(html){
if(!html){
return [];
}
var a = [];
var aRegex = [
/<a.*?href=['"]([^"']*)['"][^>]*>/gmi,
/<script.*?src=['"]([^"']*)['"][^>]*>/gmi,
/<link.*?href=['"]([^"']*)['"][^>]*>/gmi,
/<img.*?src=['"]([^"']*)['"][^>]*>/gmi,
/url\s*\([\\'"]*([^\(\)]+)[\\'"]*\)/gmi, //CSS背景
];
html = html.replace(/[\n\r\t]/gm,'');
for(var i = 0; i < aRegex.length; i++){
do{
var aRet = aRegex[i].exec(html);
if(aRet){
this.debug && this.oFile.save("_log/aParseUrl.log",aRet.join("\n")+"\n\n","utf8",function(){},true);
a.push(aRet[1].trim().replace(/^\/+/,'')); //删除/是否会产生问题
}
}while(aRet);
}
return a;
};
/**
* @desc 判断请求资源类型
*
* @param string Content-Type头内容
*
* @return [大分类,小分类,编码类型] ["image","png","utf8"]
*/
Robot.prototype.getResourceType = function(type){
if(!type){
return '';
}
var aType = type.split('/');
aType.forEach(function(s,i,a){
a[i] = s.toLowerCase();
});
if(aType[1] && (aType[1].indexOf(';') > -1)){
var aTmp = aType[1].split(';');
aType[1] = aTmp[0];
for(var i = 1; i < aTmp.length; i++){
if(aTmp[i] && (aTmp[i].indexOf("charset") > -1)){
aTmp2 = aTmp[i].split('=');
aType[2] = aTmp2[1] ? aTmp2[1].replace(/^\s+|\s+$/,'').replace('-','').toLowerCase() : '';
}
}
}
if((["image"]).indexOf(aType[0]) > -1){
aType[2] = "binary";
}
return aType;
};
/**
* @desc 抓取页面内容成功调用的回调函数
*
* @param string str 抓取的内容
* @param [] aType 抓取内容类型
* @param string url 请求的URL地址
*
* @return void
*/
Robot.prototype.handlerSuccess = function(str,aType,url){
if((aType[0] === "text") && ((["css","html"]).indexOf(aType[1]) > -1)){ //提取URL地址
aUrls = (url.indexOf(this.domain) > -1) ? this.parseUrl(str) : []; //非站内只抓取一次
for(var i = 0; i < aUrls.length; i++){
if(!this.oUrl.isValidPart(aUrls[i])){
this.debug && this.oFile.save("_log/aInvalidRawUrl.log",url+"----"+aUrls[i]+"\n","utf8",function(){},true);
continue;
}
var sUrl = this.oUrl.fix(url,aUrls[i]);
/*if(sUrl.indexOf(this.domain) === -1){ //只抓取站点内的 这里判断会过滤掉静态资源
continue;
}*/
if(aNewUrlQueue.indexOf(sUrl) > -1){
continue;
}
if(aGotUrlQueue.indexOf(sUrl) > -1){
continue;
}
aNewUrlQueue.push(sUrl);
}
}
//内容存文件
var sPath = this.oUrl.getUrlPath(url);
var self = this;
var oTmp = urlUtil.parse(url);
if(oTmp["hostname"]){//路径包含域名 防止文件保存时因文件名相同被覆盖
sPath = sPath.replace(/^\/+/,'');
sPath = oTmp["hostname"]+pathUtil.sep+sPath;
}
if(sPath){
if(this.debug){
this.oFile.save("_log/urlFileSave.log",url+"--------"+sPath+"\n","utf8",function(){},true);
}
if(sPath.length > sPathMaxSize){ //可能有问题的路径 打监控日志
this.oFile.save("_log/sPathMaxSizeOverLoad.log",url+"--------"+sPath+"\n","utf8",function(){},true);
return ;
}
if(aType[2] != "binary"){//只支持UTF8编码
aType[2] = "utf8";
}
this.oFile.save(sPath,str,aType[2] ? aType[2] : "utf8",function(err){
if(err){
self.debug && console.log("Path:"+sPath+"存文件失败");
} else {
oCnt.fSucc++;
}
});
}
oCnt.succ++;
this.crawl();//继续抓取
};
/**
* @desc 抓取页面失败调用的回调函数
*
* @return void
*/
Robot.prototype.handlerFailure = function(){
this.crawl();
};
/**
* @desc 外部引用
*/
module.exports = Robot;
调用
var Robot = require("./robot.js");
var oOptions = {
domain:'baidu.com', //抓取网站的域名
firstUrl:'http://www.baidu.com/', //抓取的初始URL地址
saveDir:"E:\\wwwroot/baidu/", //抓取内容保存目录
debug:true, //是否开启调试模式
};
var o = new Robot(oOptions);
o.crawl(); //开始抓取
后记
还有些地方需要完善
1.处理302跳转
2.处理COOKIE登陆
3.大文件偶尔会非正常退出
4.使用多进程
5.完善URL队列管理
6.异常退出之后处理
实现过程中碰到了一些问题,最后还是解决了,
爬虫原理很简单,只有真正实现过,才会对它更加理解,
原来实现不是那么简单,也是需要花时间的。
7.下载地址: https://codeload.github.com/wadeyu/nodejsrobot/zip/master
参考资料
[1]NodeJS
https://nodejs.org/
[2]Nodejs抓取非utf8字符编码的页面
http://www.cnblogs.com/fengmk2/archive/2011/05/15/2047109.html
[3]iconv-lite编码解码
https://www.npmjs.com/package/iconv-lite
一次使用NodeJS实现网页爬虫记的更多相关文章
- 基于NodeJs的网页爬虫的构建(二)
好久没写博客了,这段时间已经忙成狗,半年时间就这么没了,必须得做一下总结否则白忙.接下去可能会有一系列的总结,都是关于定向爬虫(干了好几个月后才知道这个名词)的构建方法,实现平台是Node.JS. 背 ...
- 基于NodeJs的网页爬虫的构建(一)
好久没写博客了,这段时间已经忙成狗,半年时间就这么没了,必须得做一下总结否则白忙.接下去可能会有一系列的总结,都是关于定向爬虫(干了好几个月后才知道这个名词)的构建方法,实现平台是Node.JS. 背 ...
- nodeJS实现简单网页爬虫功能
前面的话 本文将使用nodeJS实现一个简单的网页爬虫功能 网页源码 使用http.get()方法获取网页源码,以hao123网站的头条页面为例 http://tuijian.hao123.com/h ...
- nodejs 快要变成爬虫界的王者
nodejs 快要变成爬虫界的王者 爬虫这东西是很多数据采集必须要的东西. 但是现在随着网页不断发展,已经出现了出单纯的网页,到 ajax 网页, 再到 spa , 再到 websocket 应用,一 ...
- cURL 学习笔记与总结(2)网页爬虫、天气预报
例1.一个简单的 curl 获取百度 html 的爬虫程序(crawler): spider.php <?php /* 获取百度html的简单网页爬虫 */ $curl = curl_init( ...
- c#网页爬虫初探
一个简单的网页爬虫例子! html代码: <head runat="server"> <title>c#爬网</title> </head ...
- 网页爬虫--scrapy入门
本篇从实际出发,展示如何用网页爬虫.并介绍一个流行的爬虫框架~ 1. 网页爬虫的过程 所谓网页爬虫,就是模拟浏览器的行为访问网站,从而获得网页信息的程序.正因为是程序,所以获得网页的速度可以轻易超过单 ...
- 网页爬虫的设计与实现(Java版)
网页爬虫的设计与实现(Java版) 最近为了练手而且对网页爬虫也挺感兴趣,决定自己写一个网页爬虫程序. 首先看看爬虫都应该有哪些功能. 内容来自(http://www.ibm.com/deve ...
- Python 网页爬虫 & 文本处理 & 科学计算 & 机器学习 & 数据挖掘兵器谱(转)
原文:http://www.52nlp.cn/python-网页爬虫-文本处理-科学计算-机器学习-数据挖掘 曾经因为NLTK的缘故开始学习Python,之后渐渐成为我工作中的第一辅助脚本语言,虽然开 ...
随机推荐
- 【原创翻译】链接DLL至可执行文件---翻译自MSDN
可执行文件.exe链接(或加载)DLL有以下两种形式: 隐式链接 显式链接 隐式链接是指静态加载或在程序加载时动态链接. 通过隐式链接,在使用DLL时,可执行文件链接到一个由生成DLL的人提供的导入函 ...
- PHP11 日期和时间
学习要点 UNIX时间戳 将其他格式的日期转成UNIX时间戳格式 基于UNIX时间戳的日期计算 获取并格式化输出日期 修改PHP的默认时间 微秒的使用 Unix时间戳 相关概念 Unix tim ...
- PHP09 字符串和正则表达式
学习要点 字符串处理简介 常用的字符串输出函数 常用的字符串格式化函数 字符串比较函数 正则表达式简介 正则表达式语法规则 与perl兼容的正则表达式函数 字符串处理介绍 Web开发中字符串处理 ...
- sublime text 3 安装Nodejs插件
如题 1)集成Nodejs插件到sublime,地址:https://github.com/tanepiper/SublimeText-Nodejs2)解压zip文件, 并重命名文件夹“Nodejs” ...
- strong&weak
copy:建立一个索引计数为1的对象,然后释放旧对象 对NSString对NSString 它指出,在赋值时使用传入值的一份拷贝.拷贝工作由copy方法执行,此属性只对那些实行了NSCopying协议 ...
- sublime中项目无法添加文件夹
问题记录 mac中,使用vue init webpack project 后,在sublime中打开,但是添加新文件夹和删除,总提示没有权限, 然后用git提交吧 也不行,每次都要sudo 出现的提示 ...
- django实现github第三方本地登录
1.安装 pip install social-auth-app-django 2.生成Client ID和Client Secret 3.修改setting.py INSTALLED_APPS = ...
- markdown pad激活
<iframe src="></iframe> ---恢复内容开始--- 注册码 Soar360@live.com GBPduHjWfJU1mZqcPM3BikjYK ...
- 第六天,字典Dictionary
字典(Dictionary)在Python中是一种可变的容器模型,它是通过一组键(key)值(value)对组成,这种结构类型通常也被称为映射,或者叫关联数组,也有叫哈希表的.每个key-value之 ...
- react-native打包apk常见错误收集
react-native 0.59打包报错,信息如下,根据错误信息是因为react-native-cookies的sdk版本问题导致的 ./gradlew assembleRelease > C ...