使用node爬蟲,爬取指定排名網站的JS引用庫

前期準備

本爬蟲將從網站爬取排名前幾的網站,具體前幾名能夠具體設置,並分別爬取他們的主頁,檢查是否引用特定庫。html

github地址前端

所用到的node主要模塊node

  • express 不用多說jquery

  • request http模塊git

  • cheerio 運行在服務器端的jQuerygithub

  • node-inspector node調試模塊web

  • node-dev 修改文件後自動重啓app算法

關於調試Node

在任意一個文件夾,執行node-inspector,經過打開特定頁面,在頁面上進行調試,而後運行app,使用node-dev app.js來自動重啓應用。數據庫

所碰到的問題

1. request請求多個頁面

  1. 因爲請求是異步執行的,和分別返回3個頁面的數據,這裏只爬取了50個網站,一個頁面有20個,因此有3頁,經過循環裏套request請求,來實現。express

  2. 經過添加請求頭能夠實現基本的反爬蟲

  3. 處理數據的方法都寫在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;

後記

經過這個小爬蟲,學習到不少知識,例如爬蟲的反爬蟲有哪些策越,意識到node.js的異步執行特性,先後端是怎麼進行交互的。同時,也意識到有一些方面的不足,後面還須要繼續改進,歡迎你們的相互交流。

相關文章
相關標籤/搜索