一次使用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,之后渐渐成为我工作中的第一辅助脚本语言,虽然开 ...
随机推荐
- asp.net mvc 5 微信接入VB版 - 获取AccessToken
获取AccessToken是微信接入的又一个基础操作.很多微信接口需要这个2小时一刷新的AccessToken作为参数. 转载请说明作者Nukepayload2 首先根据开发文档把获取AccessTo ...
- 基于Zabbix API文档二次开发与java接口封装
(继续贴一篇之前工作期间写的经验案例) 一. 案例背景 我负责开发过一个平台的监控报警模块,基于zabbix实现,需要对zabbix进行二次开发. Zabbix官方提供了Rest ...
- 网页添加tittle前的图标logo
在head标签中 <link rel="icon" href="~/favicon.ico" type="image/x-icon" ...
- C-基础:数组名与取地址符&
指出下面代码的输出,并解释为什么.(不错,对地址掌握的深入挖潜) main() { ]={,,,,}; ); printf(),*(ptr-)); } 输出:2,5 *(a+1)就是a[1], ...
- 【软件构造】第三章第四节 面向对象编程OOP
第三章第四节 面向对象编程OOP 本节讲学习ADT的具体实现技术:OOP Outline OOP的基本概念 对象 类 接口 抽象类 OOP的不同特征 封装 继承与重写(override) 多态与重载( ...
- 利用JS动态生成隔行换色HTML表格
用JS生成动态生成表格,行.列由用户输入,并使表格隔行换色 方法一. 代码: <!DOCTYPE html> 2 <html> 3 <head> 4 <tit ...
- noip2019——动态规划刷题历程
加粗的是值得总结的 从洛谷的普及题开始刷题: 背包式dp(有些技巧的) 1.p2639[USACO09OCT]Bessie的体重问题 -p1049取模意义下01背包 技巧:重量=价值 2.金明的预算问 ...
- linux系统日志中出现大量systemd Starting Session ### of user root 解决
这种情况是正常的,不算是一个问题 https://access.redhat.com/solutions/1564823 Environment Red Hat Enterprise Linux 7 ...
- python基础(二) —— 流程控制语句
编程语言中的流程控制语句分为以下几类: 顺序语句 分支语句 循环语句 其中顺序语句不需要单独的关键字来控制,就是按照先后顺序一行一行的执行,不需要特殊的说明. 下面主要是 分支语句 和 循环语句的说明 ...
- 分分钟钟学会Python - 函数(function)
函数(function) 1 基本结构 本质:将多行代码拿到别处并起个名字,以后通过名字就可以找到这行代码并执行 应用场景: 代码重复执行 代码量很多超过一屏,可以选择通过函数进行代码的分割 写代码方 ...