1. 程式人生 > >Nodejs 實現爬蟲的改造:Promise優化、動態頁面資料的獲取、多個頁面併發爬取

Nodejs 實現爬蟲的改造:Promise優化、動態頁面資料的獲取、多個頁面併發爬取

跟著Scott老師把上一次的那個爬蟲程式碼進行改造,主要包括單個網頁爬取變為多個網頁爬取、使用Promise來優化多層回撥、動態資料的獲取(Scott老師視訊中沒有的,自己亂搞一個晚上出來的。。。) 

首先來介紹一下Promise,Promise可以將多層的回撥轉換為鏈式來操作,大大提升了程式碼的可讀性與維護性。從表面上看,Promise只是能夠簡化層層回撥的寫法,而實質上,Promise的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞callback函式要簡單、靈活的多。下面來介紹如何使用Promise來優化

1. 由於該爬蟲是多個頁面併發爬取的,使用普通的方法需要層層回撥,所以對該回調函式(獲取頁面資料函式)進行Promise包裝

function getPageAsync(url) {//使用Promise物件來包裝獲取到頁面的html的方法
    return new Promise(function (resolve,reject) {
        console.log('正在爬取 ' + url + '\n');

        http.get(url,function(res){
            var html = '';

            res.on('data',function (data) {
                html += data.toString('utf-8');
            })
            res.on('end',function(){
                resolve(html);//把當前的獲取到頁面的html返回回去(傳遞下去)
            })

        }).on('error',function (e) {
            reject(e);
            console.log("獲取課程資料出錯!");
        })
    })
}

2. 使用Promise的all方法(引數是一個數組,當這個數組裡面所有的 Promise 物件都變為 resolve 時,該方法才會返回)來併發獲取每個頁面的原始碼,然後執行then方法來執行每個頁面的爬取操作
Promise
    .all(fetchPageUrl)//針對每個url地址返回的頁面HTML原始碼併發操作進行爬取
    .then(function (Pages) {
        var coursesData = [];
        Pages.forEach(function (html) {
            var course = selecttHtml(html);//獲取當前爬取的資料
            coursesData.push(course);//儲存當前爬取的資料
        })
        //console.log(courseMembers);
        for(var i in courseIds)//獲取每個課程的學習人數,因為獲取是非同步操作的,所以要用同步地給每個課程物件賦值
        {
            for(var j in courseMembers.id)
                if(courseMembers.id[j] === courseIds[i]) {
                    coursesData[i].number = courseMembers.numbers[j];
                }
        }
       coursesData.sort(function (a, b) {//按照學習的人數從高到低排序
            return a.number < b.number;
        })
        printinfo(coursesData);//列印已經爬取好的資料
    })


動態資料的獲取,因為慕課網頁面原始碼的改變,原本Scott老師視訊中獲取的學習人數是靜態資料來的,現在卻是動態資料(好心塞啊。。),下面我來介紹一下自己是怎麼獲取到的動態資料的

1.首先在頁面原始碼中找到學習人數這個數值的標籤位置,發現它由一個特有的類(js-learn-num)來控制的。

2. 然後在開發人員工具中的除錯程式面板下搜尋js-learn-num,然後發現該資料是通過ajax的GET方法來非同步獲取的(如下圖所示)


3. 在網路面板中搜索上圖的url地址,然後找到一個AjaxCourseMembers?ids=259的請求,裡面用JSON格式來封裝的就是我們需要獲取學習人數的動態資料

4. 下面直接用Nodejs中http模組的get方法去獲取這個JSON資料,然後進行JSON解析該資料從而獲得我們想要的資料

下面直接來看程式碼:

/**
 * Created by Turne on 2017/2/10.
 */

var http = require('http');
var Promise = require('bluebird')
var querystring = require('querystring');
var url = 'http://www.imooc.com/course/AjaxCourseMembers?ids=728';
var titleBaseUrl = 'http://www.imooc.com/course/AjaxCourseMembers?ids=';//用以獲取每個課程的學習人數,該資料是動態的
var cheerio = require('cheerio');
var baseUrl = 'http://www.imooc.com/learn/';
var courseIds = [728,637,348,259,197,134,75];//需要爬取課程的id
var courseMembers = {id:[],numbers:[]};//每個課程學習的人數

function printinfo(coursesData) {//列印已經爬好的東西
    coursesData.forEach(function (courseData) {
        console.log(courseData.number + " 學過了 " + courseData.title + '\n');
    })
    var chapterTitle;
    coursesData.forEach(function (courseDatas) {
        console.log('###'+courseDatas.title +'###'+ '\n');//列印每個課程的標題
        courseDatas.courseData.forEach(function (item) {
            chapterTitle  = item.chapterTitle;
            console.log(chapterTitle + '\n');//列印每一章的標題

            item.videos.forEach(function (video) {
                console.log(' 【' + video.id + '】 '+ video.title + '\n');//列印每個視訊的id和標題
            })
        })
    })
}

function selecttHtml(html) {//通過頁面原始碼來選擇需要爬取的東西
    var $ = cheerio.load(html);
    var contents = $('.chapter');//某章節下的HTML的原始碼
    var title = $($('.course-infos')).find('h2').text();//整個課程的大標題
    var id = $($(".course-infos").find('a')[3]).attr('href').split('/learn/')[1];
    //getCourseMembers(parseInt(id,10));
    //console.log(number);
    //var courseData = [];

    var coursesData = {
        title:title,
        number:0,
        courseData:[]
    }

    contents.each(function (item) {
        var content = $(this);//當前這一章的HTML原始碼資料
        var text = content.find('.chapter-content').text();
        var chapterTitle = content.find('strong').text().split(text)[0].trim();//獲取每一章的標題
        var videos = content.find('.video').children('li');//獲取每個視訊的資訊,包含視訊的id和標題

        var chapterData = {
            chapterTitle:chapterTitle,
            videos: []
        };

        videos.each(function (item) {
            var video = $(this).find('a');
            var title = video.text().split('開始學習')[0].trim();//獲取每個視訊的標題
            //console.log(title.length);
            title = title.substring(0,title.length - 10).trim() + " " + title.substring(title.length - 10,title.length).trim();
            var id = video.attr('href').split('video/')[1];//獲取每個視訊的id

            chapterData.videos.push({
                title:title,
                id:id
            })

        })

        coursesData.courseData.push(chapterData)//儲存爬取的資料
    })
    return coursesData;
}

function getPageAsync(url) {//使用Promise物件來包裝獲取到頁面的html的方法
    return new Promise(function (resolve,reject) {
        console.log('正在爬取 ' + url + '\n');

        http.get(url,function(res){
            var html = '';

            res.on('data',function (data) {
                html += data.toString('utf-8');
            })
            res.on('end',function(){
                resolve(html);//把當前的獲取到頁面的html返回回去(傳遞下去)
            })

        }).on('error',function (e) {
            reject(e);
            console.log("獲取課程資料出錯!");
        })
    })
}

function getCourseMembers(id) {//用以獲取每個課程的學習人數
    var url = titleBaseUrl + id;
    var members;
    //由於學習人數是通過AjAX來非同步更新的,所以我們要使用http的個get方法去獲取AJAX獲取資料的url去獲得我們想要的資料
    http.get(url,function(res){
        var datas = '';

        res.on('data',function (chunk) {
            datas += chunk;
        })
        res.on('end',function(){
            datas = JSON.parse(datas);//由於獲取到的資料是JSON格式的,所以需要JSON.parse方法淺解析
            courseMembers.id.push(id);//儲存每個課程的
            courseMembers.numbers.push(parseInt(datas.data[0].numbers,10));//儲存每個課程的學習人數
        })

    })
}

var fetchPageUrl = [];

courseIds.forEach(function (id) {
    fetchPageUrl.push(getPageAsync(baseUrl + id));
    getCourseMembers(id);
})


Promise
    .all(fetchPageUrl)//針對每個url地址返回的頁面HTML原始碼併發操作進行爬取
    .then(function (Pages) {
        var coursesData = [];
        Pages.forEach(function (html) {
            var course = selecttHtml(html);//獲取當前爬取的資料
            coursesData.push(course);//儲存當前爬取的資料
        })
        //console.log(courseMembers);
        for(var i in courseIds)//獲取每個課程的學習人數,因為獲取是非同步操作的,所以要用同步地給每個課程物件賦值
        {
            for(var j in courseMembers.id)
                if(courseMembers.id[j] === courseIds[i]) {
                    coursesData[i].number = courseMembers.numbers[j];
                }
        }
       coursesData.sort(function (a, b) {//按照學習的人數從高到低排序
            return a.number < b.number;
        })
        printinfo(coursesData);//列印已經爬取好的資料
    })