Nodejs 實現爬蟲的改造:Promise優化、動態頁面資料的獲取、多個頁面併發爬取
阿新 • • 發佈:2019-01-24
跟著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);//列印已經爬取好的資料
})