利用node.js爬取指定排名网站的JS引用库详解
前言
本文给大家介绍的爬虫将从网站爬取排名前几的网站,具体前几名可以具体设置,并分别爬取他们的主页,检查是否引用特定库。下面话不多说了,来一起看看详细的介绍:
所用到的node主要模块
- express 不用多说
- request http模块
- cheerio 运行在服务器端的jquery
- node-inspector node调试模块
- node-dev 修改文件后自动重启app
关于调试node
在任意一个文件夹,执行node-inspector,通过打开特定页面,在页面上进行调试,然后运行app,使用node-dev app.js来自动重启应用。
所碰到的问题
1. request请求多个页面
由于请求是异步执行的,和分别返回3个页面的数据,这里只爬取了50个网站,一个页面有20个,所以有3页,通过循环里套request请求,来实现。
通过添加请求头可以实现基本的反爬虫
处理数据的方法都写在analydata()
里面,造成后面的数据重复存储了,想了很久,才想到一个解决方法,后面会写到是怎么解决的。
for (var i = 1; i < len+1; i++) { (function(i){ var options = { url: 'http://www.alexa.cn/siterank/' + i, headers: { 'user-agent': 'mozilla/5.0 (windows nt 10.0; wow64) applewebkit/537.36 (khtml, like gecko) chrome/59.0.3071.115 safari/537.36' } }; request(options, function (err, response, body) { analydata(body,rank); }) })(i) }
2. 多层回调
仔细观察代码,你会发现,处理数据的方法使用了如下的多层回调,也可以不使用回调,写在一个函数内部;因为,每层都要使用上一层的数据,造成了这样的写法。
function f1(data1){ f2(data1); } function f2(data2){ f3(data2); } function f3(data3){ f4(data4); }
3. 正则获取js库
由于获取页面库,首先需要获取到script的src属性,然后通过正则来实现字符串匹配。
<script src="https://ss0.bdstatic.com/5av1bjqh_q23odcf/static/superman/js/lib/jquery-1.10.2_d88366fd.js"></script>
获取到的script可能是上面这样的,由于库名的命名真是各种各样,后来想了一下,因为文件名是用.js结尾的,所以就以点号为结尾,然后把点号之前的字符截取下来,这样获得了库名,代码如下。
var reg = /[^\/\\]+$/g; var libname = jslink.match(reg).join(''); var libfilter = libname.slice(0,libname.indexof('.'));
4.cheerio模块获取js引用链接
这部分也花了一点时间,才搞定,cheerio获取dom的方法和jquery是一样的,需要对返回的dom对象进行查看,就可以看到对象里隐藏好深的href属性,方法大同小异,你也可以使用其他选择器,选择到script标签
var $ = cheerio.load(body); var scriptfile = $('script').toarray(); scriptfile.foreach(function(item,index){ if (item.attribs.src != null) { obtainlibname(item.attribs.src,index); }
5.存储数据到数据库
存储数据的逻辑是先获取所有的script信息,然后push到一个缓存数组,由于push后面,紧跟着存储到数据库的方法,这两个方法都写在循环里面的,例如爬取5个网站,每个网站存储一次,后面也会跟着存储,造成数据重复存储。解决方法是存储数据的一般逻辑是先查,再存,这个查比较重要,查询的方法也有多种,这里主要是根据库名来查找唯一的数据对象,使用findone方法。注意,由于node.js是异步执行的,这里的闭包,每次只传一个i值进去,执行存储的操作。
// 将缓存数据存储到数据库 function store2db(libobj){ console.log(libobj); for (var i = 0; i < libobj.length; i++) { (function(i){ var jslib = new jslib({ name: libobj[i].lib, libsnum: libobj[i].num }); jslib.findone({'name': libobj[i].lib},function(err,libdoc){ if(err) console.log(err); // console.log(libdoc) if (!libdoc){ jslib.save(function(err,result){ if(err) console.log('保存数据出错' + err); }); } }) })(i) } console.log('一共存储' + libobj.length + '条数据到数据库'); }
6.分页插件
本爬虫前端使用了bootstrap.paginator插件,主要是前台分页,返回数据,根据点击的页数,来显示对应的数据,后期考虑使用ajax请求的方式来实现翻页的效果,这里的注意项,主要是最后一页的显示,最好前面做个判断,因为返回的数据,不一定刚好是页数的整数倍
function _paging(libobj) { var ele = $('#page'); var pages = math.ceil(libobj.length/20); console.log('总页数' + pages); ele.bootstrappaginator({ currentpage: 1, totalpages: pages, size:"normal", bootstrapmajorversion: 3, alignment:"left", numberofpages:pages, itemtexts: function (type, page, current) { switch (type) { case "first": return "首页"; case "prev": return "上一页"; case "next": return "下一页"; case "last": return "末页"; case "page": return page; } }, onpageclicked: function(event, originalevent, type, page){ // console.log('当前选中第:' + page + '页'); var phtml = ''; var endpage; var startpage = (page-1) * 20; if (page < pages) { endpage = page * 20; }else{ endpage = libobj.length; } for (var i = startpage; i < endpage; i++) { phtml += '<tr><td>'; phtml += (i+1) + '</td><td>'; phtml += libobj[i].name + '</td><td>'; phtml += libobj[i].libsnum + '</td></tr>'; } libshow.html(phtml); } }) }
完整代码
1. 前端
$(function () { var query = $('.query'), rank = $('.rank'), show = $('.show'), querylib = $('.querylib'), libshow = $('#libshow'), libname = $('.libname'), displayresult = $('.displayresult'); var checklib = (function(){ function _query(){ query.click(function(){ $.post( '/query', { rank: rank.val(), }, function(data){ console.log(data); } ) }); querylib.click(function(){ var inputlibname = libname.val(); if (inputlibname.length == 0) { alert('请输入库名~'); return; } $.post( '/querylib', { libname: inputlibname, }, function(data){ if(data.length == 0){ alert('没有查询到名为' + inputlibname + '的库'); libname.val(''); libname.focus(); libshow.html('') return; } var libhtml = ''; for (var i = 0; i < data.length; i++) { libhtml += '<tr><td>'; libhtml += (i+1) + '</td><td>'; libhtml += data[i].name + '</td><td>'; libhtml += data[i].libsnum + '</td></tr>'; } libshow.html(libhtml); } ) }); } function _showlibs(){ show.click(function(){ $.get( '/getlibs', { rank: rank.val(), }, function(data){ console.log('一共返回'+ data.length + '条数据'); console.log(data) var libhtml = ''; for (var i = 0; i < 20; i++) { libhtml += '<tr><td>'; libhtml += (i+1) + '</td><td>'; libhtml += data[i].name + '</td><td>'; libhtml += data[i].libsnum + '</td></tr>'; } displayresult.show(); libshow.html(libhtml);// 点击显示按钮,显示前20项数据 _paging(data); } ) }); } //翻页器 function _paging(libobj) { var ele = $('#page'); var pages = math.ceil(libobj.length/20); console.log('总页数' + pages); ele.bootstrappaginator({ currentpage: 1, totalpages: pages, size:"normal", bootstrapmajorversion: 3, alignment:"left", numberofpages:pages, itemtexts: function (type, page, current) { switch (type) { case "first": return "首页"; case "prev": return "上一页"; case "next": return "下一页"; case "last": return "末页"; case "page": return page; } }, onpageclicked: function(event, originalevent, type, page){ // console.log('当前选中第:' + page + '页'); var phtml = ''; var endpage; var startpage = (page-1) * 20; if (page < pages) { endpage = page * 20; }else{ endpage = libobj.length; } for (var i = startpage; i < endpage; i++) { phtml += '<tr><td>'; phtml += (i+1) + '</td><td>'; phtml += libobj[i].name + '</td><td>'; phtml += libobj[i].libsnum + '</td></tr>'; } libshow.html(phtml); } }) } function init() { _query(); _showlibs(); } return { init: init } })(); checklib.init(); })
2.后端路由
var express = require('express'); var mongoose = require('mongoose'); var request = require('request'); var cheerio =require('cheerio'); var router = express.router(); var jslib = require('../model/jslib') /* 显示主页 */ router.get('/', function(req, res, next) { res.render('index'); }); // 显示库 router.get('/getlibs',function(req,res,next){ jslib.find({}) .sort({'libsnum': -1}) .exec(function(err,data){ res.json(data); }) }) // 库的查询 router.post('/querylib',function(req,res,next){ var libname = req.body.libname; jslib.find({ name: libname }).exec(function(err,data){ if (err) console.log('查询出现错误' + err); res.json(data); }) }) router.post('/query',function(req,res,next) { var rank = req.body.rank; var len = math.round(rank/20); for (var i = 1; i < len+1; i++) { (function(i){ var options = { url: 'http://www.alexa.cn/siterank/' + i, headers: { 'user-agent': 'mozilla/5.0 (windows nt 10.0; wow64) applewebkit/537.36 (khtml, like gecko) chrome/59.0.3071.115 safari/537.36' } }; request(options, function (err, response, body) { analydata(body,rank); }) })(i) } res.json('保存成功') }) var sites = []; var flag = 0; function analydata(data,rank) { if(data.indexof('html') == -1) return false; var $ = cheerio.load(data);// 传递 html var sitesarr = $('.info-wrap .domain-link a').toarray();//将所有a链接存为数组 console.log('网站爬取中``') for (var i = 0; i < 10; i++) { // ***这里后面要改,默认爬取前10名 var url = sitesarr[i].attribs.href; sites.push(url);//保存网址,添加wwww前缀 } console.log(sites); console.log('一共爬取' + sites.length +'个网站'); console.log('存储数据中...') getscript(sites); } // 获取js库文件地址 function getscript(urls) { var scriptarr = []; var src = []; var jssrc = []; for (var j = 0; j < urls.length; j++) { (function(i,callback){ var options = { url: urls[i], headers: { 'user-agent': 'mozilla/5.0 (windows nt 10.0; wow64) applewebkit/537.36 (khtml, like gecko) chrome/59.0.3071.115 safari/537.36' } } request(options, function (err, res, body) { if(err) console.log('出现错误: '+err); var $ = cheerio.load(body); var scriptfile = $('script').toarray(); callback(scriptfile,options.url); }) })(j,storelib) }; function storelib(scriptfile,url){ flag++;// 是否存储数据的标志 scriptfile.foreach(function(item,index){ if (item.attribs.src != null) { obtainlibname(item.attribs.src,index); } }) function obtainlibname(jslink,i){ var reg = /[^\/\\]+$/g; var libname = jslink.match(reg).join(''); var libfilter = libname.slice(0,libname.indexof('.')); src.push(libfilter); } // console.log(src.length); // console.log(calcnum(src).length) (function(len,urllength,src){ // console.log('length is '+ len) if (len == 10 ) {// len长度为url的长度才向src和数据库里存储数据,防止重复储存 // calcnum(src);//存储数据到数据库 // ***这里后面要改,默认爬取前10名 var libsrc = calcnum(src); store2db(libsrc); } })(flag,urls.length,src) } }// getscript end // 将缓存数据存储到数据库 function store2db(libobj){ console.log(libobj); for (var i = 0; i < libobj.length; i++) { (function(i){ var jslib = new jslib({ name: libobj[i].lib, libsnum: libobj[i].num }); jslib.findone({'name': libobj[i].lib},function(err,libdoc){ if(err) console.log(err); // console.log(libdoc) if (!libdoc){ jslib.save(function(err,result){ if(err) console.log('保存数据出错' + err); }); } }) })(i) } console.log('一共存储' + libobj.length + '条数据到数据库'); } // js库排序算法 function calcnum(arr){ var libobj = {}; var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (libobj[arr[i]]) { libobj[arr[i]] ++; } else { libobj[arr[i]] = 1; } } for(var o in libobj){ result.push({ lib: o, num: libobj[o] }) } result.sort(function(a,b){ return b.num - a.num; }); return result; } module.exports = router;
源码下载
github下载地址 ()
后记
通过这个小爬虫,学习到很多知识,例如爬虫的反爬虫有哪些策越,意识到node.js的异步执行特性,前后端是怎么进行交互的。同时,也意识到有一些方面的不足,后面还需要继续改进,欢迎大家的相互交流。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。
上一篇: 解析秦朝的孟姜女为什么会哭倒长城之谜
下一篇: 让我歇会就好了