從0到1學習node之簡易的網絡爬蟲

咱們這節的目標是學習完本節課程後,能進行網頁簡單的分析與抓取,對抓取到的信息進行輸出和文本保存。html

爬蟲的思路很簡單:node

  1. 肯定要抓取的URL;
  2. 對URL進行抓取,獲取網頁內容;
  3. 對內容進行分析並存儲;
  4. 重複第1步

本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.htmlmysql

總索引:git

  1. 從0到1學習node(一)之模塊規範
  2. 從0到1學習node(二)之搭建http服務器
  3. 從0到1學習node(三)之文件操做
  4. 從0到1學習node(四)之簡易的網絡爬蟲
  5. 從0到1學習node(五)之mysql數據庫的操做

在這節裏作爬蟲,咱們使用到了兩個重要的模塊:github

  • request : 對http進行封裝,提供更多、更方便的接口供咱們使用,request進行的是異步請求。更多信息能夠去[request-github]上進行查看
  • cheerio : 相似於jQuery,能夠使用$(), find(), text(), html()等方法提取頁面中的元素和數據,不過若仔細比較起來,cheerio中的方法不如jQuery的多。

1. hello world

說是hello world,其實首先開始的是最簡單的抓取。咱們就以cnode網站爲例(https://cnodejs.org/),這個網站的特色是:ajax

  • 不須要登陸便可訪問首頁和其餘頁面
  • 頁面都是同步渲染的,沒有異步請求的問題
  • DOM結構清晰

代碼以下:算法

var request = require('request'),
    cheerio = require('cheerio');

request('https://cnodejs.org/', function(err, response, body){
    if( !err && response.statusCode == 200 ){
        // body爲源碼
        // 使用 cheerio.load 將字符串轉換爲 cheerio(jQuery) 對象,
        // 按照jQuery方式操做便可
        var $ = cheerio.load(body);
        
        // 輸出導航的html代碼
        console.log( $('.nav').html() );
    }
});

這樣的一段代碼就實現了一個簡單的網絡爬蟲,爬取到源碼後,再對源碼進行拆解分析,好比咱們要獲取首頁中第1頁的 問題標題,做者,跳轉連接,點擊數量,回覆數量。經過chrome,咱們能夠獲得這樣的結構:sql

每一個div[.cell]是一個題目完整的單元,在這裏面,一個單元暫時稱爲$itemchrome

{
    title : $item.find('.topic_title').text(),
    url : $item.find('.topic_title').attr('href'),
    author : $item.find('.user_avatar img').attr('title'),
    reply : $item.find('.count_of_replies').text(),
    visits : $item.find('.count_of_visits').text()
}

所以,循環div[.cell],就能夠獲取到咱們想要的信息了:數據庫

request('https://cnodejs.org/?_t='+Date.now(), function(err, response, body){
    if( !err && response.statusCode == 200 ){
        var $ = cheerio.load(body);

        var data = [];
        $('#topic_list .cell').each(function(){
            var $this = $(this);
            
            // 使用trim去掉數據兩端的空格
            data.push({
                title : trim($this.find('.topic_title').text()),
                url : trim($this.find('.topic_title').attr('href')),
                author : trim($this.find('.user_avatar img').attr('title')),
                reply : trim($this.find('.count_of_replies').text()),
                visits : trim($this.find('.count_of_visits').text())
            })
        });
        // console.log( JSON.stringify(data, ' ', 4) );
        console.log(data);
    }
});

// 刪除字符串左右兩端的空格
function trim(str){ 
    return str.replace(/(^\s*)|(\s*$)/g, "");
}

2. 爬取多個頁面

上面咱們只爬取了一個頁面,怎麼在一個程序裏爬取多個頁面呢?仍是以CNode網站爲例,剛纔只是爬取了第1頁的數據,這裏咱們想請求它前6頁的數據(別同時抓太多的頁面,會被封IP的)。每一個頁面的結構是同樣的,咱們只須要修改url地址便可。

2.1 同時抓取多個頁面

首先把request請求封裝爲一個方法,方便進行調用,若仍是使用console.log方法的話,會把6頁的數據都輸出到控制檯,看起來很不方便。這裏咱們就使用到了上節文件操做內容,引入fs模塊,將獲取到的內容寫入到文件中,而後新建的文件放到file目錄下(需手動建立file目錄):

// 把page做爲參數傳遞進去,而後調用request進行抓取
function getData(page){
    var url = 'https://cnodejs.org/?tab=all&page='+page;
    console.time(url);
    request(url, function(err, response, body){
        if( !err && response.statusCode == 200 ){
            console.timeEnd(url); // 經過time和timeEnd記錄抓取url的時間

            var $ = cheerio.load(body);

            var data = [];
            $('#topic_list .cell').each(function(){
                var $this = $(this);

                data.push({
                    title : trim($this.find('.topic_title').text()),
                    url : trim($this.find('.topic_title').attr('href')),
                    author : trim($this.find('.user_avatar img').attr('title')),
                    reply : trim($this.find('.count_of_replies').text()),
                    visits : trim($this.find('.count_of_visits').text())
                })
            });
            // console.log( JSON.stringify(data, ' ', 4) );
            // console.log(data);
            var filename = './file/cnode_'+page+'.txt';
            fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
                console.log( filename + ' 寫入成功' );
            })
        }
    });
}

CNode分頁請求的連接:https://cnodejs.org/?tab=all&page=2,咱們只須要修改page的值便可:

var max = 6;
for(var i=1; i<=max; i++){

    getData(i);
}

這樣就能同時請求前6頁的數據了,執行文件後,會輸出每一個連接抓取成功時消耗的時間,抓取成功後再把相關的信息寫入到文件中:

$ node test.js
開始請求...
https://cnodejs.org/?tab=all&page=1: 279ms
./file/cnode_1.txt 寫入成功
https://cnodejs.org/?tab=all&page=3: 372ms
./file/cnode_3.txt 寫入成功
https://cnodejs.org/?tab=all&page=2: 489ms
./file/cnode_2.txt 寫入成功
https://cnodejs.org/?tab=all&page=4: 601ms
./file/cnode_4.txt 寫入成功
https://cnodejs.org/?tab=all&page=5: 715ms
./file/cnode_5.txt 寫入成功
https://cnodejs.org/?tab=all&page=6: 819ms
./file/cnode_6.txt 寫入成功

咱們在file目錄下就能看到輸出的6個文件了。

2.2 控制同時請求的數量

咱們在使用for循環後,會同時發起全部的請求,若是咱們同時去請求100、200、500個頁面呢,會形成短期內對服務器發起大量的請求,最後就是被封IP。這裏我寫了一個調度方法,每次同時最多隻能發起5個請求,上一個請求完成後,再從隊列中取出一個進行請求。

/*
  @param data []  須要請求的連接的集合
  @param max  num 最多同時請求的數量
*/
function Dispatch(data, max){
    var _max = max || 5, // 最多請求的數量
        _dataObj = data || [], // 須要請求的url集合
        _cur = 0, // 當前請求的個數
        _num = _dataObj.length || 0,
        _isEnd = false,
        _callback;

    var ss = function(){
        var s = _max - _cur;
        while(s--){
            if( !_dataObj.length ){
                _isEnd = true;
                break;
            }
            var surl = _dataObj.shift();
            _cur++;

            _callback(surl);
        }
    }

    this.start = function(callback){
        _callback = callback;

        ss();
    },

    this.call = function(){
        if( !_isEnd ){
            _cur--;
            ss();
        }
    }
}

var dis = new Dispatch(urls, max);
dis.start(getData);

而後在 getData 中,寫入文件的後面,進行dis的回調調用:

var filename = './file/cnode_'+page+'.txt';
fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
    console.log( filename + ' 寫入成功' );
})
dis.call();

這樣就實現了異步調用時控制同時請求的數量。

3. 抓取須要登陸的頁面

好比咱們在抓取CNode,百度貼吧等一些網站,是不須要登陸就能夠直接抓取的,那麼如知乎等網站,必須登陸後才能抓取,不然直接跳轉到登陸頁面。這種狀況咱們該怎麼抓取呢?

使用cookie。 用戶登陸後,都會在cookie中記錄下用戶的一些信息,咱們在抓取一些頁面,帶上這些cookie,服務器就會認爲咱們處於登陸狀態,程序就能抓取到咱們想要的信息。

先在瀏覽器上登陸咱們的賬號,而後在console中使用document.domain獲取到全部cookie的字符串,複製到下方程序的cookie處(若是你知道哪些cookie不須要,能夠剔除掉)。

request({
    url:'https://www.zhihu.com/explore',
    headers:{
        // "Referer":"www.zhihu.com"
        cookie : xxx
    }
}, function(error, response, body){
    if (!error && response.statusCode == 200) {
        // console.log( body );
        var $ = cheerio.load(body);


    }
})

同時在request中,還能夠設定referer,好比有的接口或者其餘數據,設定了referer的限制,必須在某個域名下才能訪問。那麼在request中,就能夠設置referer來進行僞造。

4. 保存抓取到的圖片

頁面中的文本內容能夠提煉後保存到文本或者數據庫中,那麼圖片怎麼保存到本地呢。

圖片能夠使用request中的pipe方法輸出到文件流中,而後使用fs.createWriteStream輸出爲圖片。

這裏咱們把圖片保存到以日期建立的目錄中,mkdirp可一次性建立多級目錄(./img/2017/01/22)。保存的圖片名稱,能夠使用原名稱,也能夠根據本身的規則進行命名。

var request = require('request'),
    cheerio = require('cheerio'),
    fs = require('fs'),
    path = require('path'), // 用於分析圖片的名稱或者後綴名
    mkdirp = require('mkdirp'); // 用於建立多級目錄

var date = new Date(),
    year = date.getFullYear(),
    month = date.getMonth()+1,
    month = ('00'+month).slice(-2), // 添加前置0
    day = date.getDate(),
    day = ('00'+day).slice(-2), // 添加前置0
    dir = './img/'+year+'/'+month+'/'+day+'/';

// 根據日期建立目錄 ./img/2017/01/22/
var stats = fs.statSync(dir);
if( stats.isDirectory() ){
    console.log(dir+' 已存在');
}else{
    console.log('正在建立目錄 '+dir);
    mkdirp(dir, function(err){
        if(err) throw err;
    })
}

request({
    url : 'http://desk.zol.com.cn/meinv/?_t='+Date.now()
}, function(err, response, body){
    if(err) throw err;

    if( response.statusCode == 200 ){
        var $ = cheerio.load(body);
        
        $('.photo-list-padding img').each(function(){
            var $this = $(this),
                imgurl = $this.attr('src');
            
            var ext = path.extname(imgurl); // 獲取圖片的後綴名,如 .jpg, .png .gif等
            var filename = Date.now()+'_'+ parseInt(Math.random()*10000)+ext; // 命名方式:毫秒時間戳+隨機數+後綴名
            // var filename = path.basename(imgurl); // 直接獲取圖片的原名稱
            // console.log(filename);
            download(imgurl, dir+filename); // 開始下載圖片
        })
    }
});

// 保存圖片
var download = function(imgurl, filename){
    request.head(imgurl, function(err, res, body) {
        request(imgurl).pipe(fs.createWriteStream(filename));
        console.log(filename+' success!');
    });
}

在對應的日期目錄裏(如./img/2017/01/22/),就能夠看到下載的圖片了。

5. 總結

咱們這裏只是寫了一個簡單的爬蟲,針對更復雜的功能,則須要更復雜的算法的來控制了。還有如何抓取ajax的數據,咱們會在後面進行講解。

本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html

相關文章
相關標籤/搜索