從零學(xué)習(xí)node.js之簡易的網(wǎng)絡(luò)爬蟲(四)
前言
之前已經(jīng)介紹了node.js的一些基本知識,下面這篇文章我們的目標(biāo)是學(xué)習(xí)完本節(jié)課程后,能進(jìn)行網(wǎng)頁簡單的分析與抓取,對抓取到的信息進(jìn)行輸出和文本保存。
爬蟲的思路很簡單:
- 確定要抓取的URL;
- 對URL進(jìn)行抓取,獲取網(wǎng)頁內(nèi)容;
- 對內(nèi)容進(jìn)行分析并存儲;
- 重復(fù)第1步
在這節(jié)里做爬蟲,我們使用到了兩個(gè)重要的模塊:
- request : 對http進(jìn)行封裝,提供更多、更方便的接口供我們使用,request進(jìn)行的是異步請求。更多信息可以去這篇文章上進(jìn)行查看
- cheerio : 類似于jQuery,可以使用$(), find(), text(), html()等方法提取頁面中的元素和數(shù)據(jù),不過若仔細(xì)比較起來,cheerio中的方法不如jQuery的多。
一、 hello world
說是hello world,其實(shí)首先開始的是最簡單的抓取。我們就以cnode網(wǎng)站為例(https://cnodejs.org/),這個(gè)網(wǎng)站的特點(diǎn)是:
- 不需要登錄即可訪問首頁和其他頁面
- 頁面都是同步渲染的,沒有異步請求的問題
- DOM結(jié)構(gòu)清晰
代碼如下:
var request = require('request'),
cheerio = require('cheerio');
request('https://cnodejs.org/', function(err, response, body){
if( !err && response.statusCode == 200 ){
// body為源碼
// 使用 cheerio.load 將字符串轉(zhuǎn)換為 cheerio(jQuery) 對象,
// 按照jQuery方式操作即可
var $ = cheerio.load(body);
// 輸出導(dǎo)航的html代碼
console.log( $('.nav').html() );
}
});
這樣的一段代碼就實(shí)現(xiàn)了一個(gè)簡單的網(wǎng)絡(luò)爬蟲,爬取到源碼后,再對源碼進(jìn)行拆解分析,比如我們要獲取首頁中第1頁的 問題標(biāo)題,作者,跳轉(zhuǎn)鏈接,點(diǎn)擊數(shù)量,回復(fù)數(shù)量。通過chrome,我們可以得到這樣的結(jié)構(gòu):
每個(gè)div[.cell]是一個(gè)題目完整的單元,在這里面,一個(gè)單元暫時(shí)稱為$item
{
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()
}
因此,循環(huán)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去掉數(shù)據(jù)兩端的空格
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, "");
}
二、爬取多個(gè)頁面
上面我們只爬取了一個(gè)頁面,怎么在一個(gè)程序里爬取多個(gè)頁面呢?還是以CNode網(wǎng)站為例,剛才只是爬取了第1頁的數(shù)據(jù),這里我們想請求它前6頁的數(shù)據(jù)(別同時(shí)抓太多的頁面,會被封IP的)。每個(gè)頁面的結(jié)構(gòu)是一樣的,我們只需要修改url地址即可。
2.1 同時(shí)抓取多個(gè)頁面
首先把request請求封裝為一個(gè)方法,方便進(jìn)行調(diào)用,若還是使用console.log方法的話,會把6頁的數(shù)據(jù)都輸出到控制臺,看起來很不方便。這里我們就使用到了上節(jié)文件操作內(nèi)容,引入fs模塊,將獲取到的內(nèi)容寫入到文件中,然后新建的文件放到file目錄下(需手動創(chuàng)建file目錄):
// 把page作為參數(shù)傳遞進(jìn)去,然后調(diào)用request進(jìn)行抓取
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的時(shí)間
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);
}
這樣就能同時(shí)請求前6頁的數(shù)據(jù)了,執(zhí)行文件后,會輸出每個(gè)鏈接抓取成功時(shí)消耗的時(shí)間,抓取成功后再把相關(guān)的信息寫入到文件中:
$ 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個(gè)文件了。
2.2 控制同時(shí)請求的數(shù)量
我們在使用for循環(huán)后,會同時(shí)發(fā)起所有的請求,如果我們同時(shí)去請求100、200、500個(gè)頁面呢,會造成短時(shí)間內(nèi)對服務(wù)器發(fā)起大量的請求,最后就是被封IP。這里我寫了一個(gè)調(diào)度方法,每次同時(shí)最多只能發(fā)起5個(gè)請求,上一個(gè)請求完成后,再從隊(duì)列中取出一個(gè)進(jìn)行請求。
/*
@param data [] 需要請求的鏈接的集合
@param max num 最多同時(shí)請求的數(shù)量
*/
function Dispatch(data, max){
var _max = max || 5, // 最多請求的數(shù)量
_dataObj = data || [], // 需要請求的url集合
_cur = 0, // 當(dāng)前請求的個(gè)數(shù)
_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 中,寫入文件的后面,進(jìn)行dis的回調(diào)調(diào)用:
var filename = './file/cnode_'+page+'.txt';
fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
console.log( filename + ' 寫入成功' );
})
dis.call();
這樣就實(shí)現(xiàn)了異步調(diào)用時(shí)控制同時(shí)請求的數(shù)量。
三、抓取需要登錄的頁面
比如我們在抓取CNode,百度貼吧等一些網(wǎng)站,是不需要登錄就可以直接抓取的,那么如知乎等網(wǎng)站,必須登錄后才能抓取,否則直接跳轉(zhuǎn)到登錄頁面。這種情況我們該怎么抓取呢?
使用cookie。 用戶登錄后,都會在cookie中記錄下用戶的一些信息,我們在抓取一些頁面,帶上這些cookie,服務(wù)器就會認(rèn)為我們處于登錄狀態(tài),程序就能抓取到我們想要的信息。
先在瀏覽器上登錄我們的帳號,然后在console中使用document.domain獲取到所有cookie的字符串,復(fù)制到下方程序的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);
}
})
同時(shí)在request中,還可以設(shè)定referer,比如有的接口或者其他數(shù)據(jù),設(shè)定了referer的限制,必須在某個(gè)域名下才能訪問。那么在request中,就可以設(shè)置referer來進(jìn)行偽造。
四、保存抓取到的圖片
頁面中的文本內(nèi)容可以提煉后保存到文本或者數(shù)據(jù)庫中,那么圖片怎么保存到本地呢。
圖片可以使用request中的pipe方法輸出到文件流中,然后使用fs.createWriteStream輸出為圖片。
這里我們把圖片保存到以日期創(chuàng)建的目錄中,mkdirp可一次性創(chuàng)建多級目錄(./img/2017/01/22)。保存的圖片名稱,可以使用原名稱,也可以根據(jù)自己的規(guī)則進(jìn)行命名。
var request = require('request'),
cheerio = require('cheerio'),
fs = require('fs'),
path = require('path'), // 用于分析圖片的名稱或者后綴名
mkdirp = require('mkdirp'); // 用于創(chuàng)建多級目錄
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+'/';
// 根據(jù)日期創(chuàng)建目錄 ./img/2017/01/22/
var stats = fs.statSync(dir);
if( stats.isDirectory() ){
console.log(dir+' 已存在');
}else{
console.log('正在創(chuàng)建目錄 '+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; // 命名方式:毫秒時(shí)間戳+隨機(jī)數(shù)+后綴名
// 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!');
});
}
在對應(yīng)的日期目錄里(如./img/2017/01/22/),就可以看到下載的圖片了。
總結(jié)
我們這里只是寫了一個(gè)簡單的爬蟲,針對更復(fù)雜的功能,則需要更復(fù)雜的算法的來控制了。還有如何抓取ajax的數(shù)據(jù),我們會在后面進(jìn)行講解。以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,小編還會繼續(xù)分享關(guān)于node入門學(xué)習(xí)的文章,感興趣的朋友們請繼續(xù)關(guān)注腳本之家。
- Node.js 利用cheerio制作簡單的網(wǎng)頁爬蟲示例
- 淺談Node.js爬蟲之網(wǎng)頁請求模塊
- 使用 Node.js 開發(fā)資訊爬蟲流程
- Node.js+jade+mongodb+mongoose實(shí)現(xiàn)爬蟲分離入庫與生成靜態(tài)文件的方法
- 基于node.js制作簡單爬蟲教程
- 利用node.js寫一個(gè)爬取知乎妹紙圖的小爬蟲
- node.js爬蟲爬取拉勾網(wǎng)職位信息
- Node.js 實(shí)現(xiàn)簡單小說爬蟲實(shí)例
- node.js實(shí)現(xiàn)博客小爬蟲的實(shí)例代碼
- Node.js爬取豆瓣數(shù)據(jù)實(shí)例分析
相關(guān)文章
Node.js進(jìn)行文件操作(讀取/寫入/修改/刪除)詳解
Node.js是一個(gè)神奇的東西,它可以讓JavaScript在服務(wù)器端運(yùn)行,讓我們的很多前端程序員也能在后端大展身手了!本文就來講講如何在Node.js中進(jìn)行文件操作:讀取、寫入、修改和刪除文件吧2023-03-03
淺析node中間件及實(shí)現(xiàn)一個(gè)簡單的node中間件
這篇文章主要介紹了淺析node中間件及實(shí)現(xiàn)一個(gè)簡單的node中間件,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
nodejs 實(shí)現(xiàn)模擬form表單上傳文件
使用nodejs來模擬form表單進(jìn)行文件上傳,可以同時(shí)上傳多個(gè)文件。2014-07-07
node登錄生成token并驗(yàn)證的實(shí)現(xiàn)
token校驗(yàn)作為項(xiàng)目里的必要項(xiàng),其重要性不言而喻,本文主要介紹了node登錄生成token并驗(yàn)證的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02
Node.js數(shù)據(jù)庫操作之連接MySQL數(shù)據(jù)庫(一)
前一陣在做項(xiàng)目的時(shí)候,需要通過nodejs連接到MySQL數(shù)據(jù)庫,于是簡單地學(xué)習(xí)了一下MySQL這個(gè)庫,分享一些學(xué)習(xí)心得給大家,希望對大家有幫助。下面這篇文章主要介紹了Node.js數(shù)據(jù)庫操作之連接MySQL數(shù)據(jù)庫的相關(guān)資料,需要的朋友可以參考下。2017-03-03
node實(shí)現(xiàn)socket鏈接與GPRS進(jìn)行通信的方法
這篇文章主要介紹了node實(shí)現(xiàn)socket鏈接與GPRS進(jìn)行通信的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
node連接MongoDB數(shù)據(jù)庫錯誤:MongoServerSelectionError:?connect?ECON
使用node連接MongoDB數(shù)據(jù)庫時(shí)發(fā)生報(bào)錯,MongoServerSelectionError:?connect?ECONNREFUSED?::1:27017,本文給大家分享原因分析及解決方案,感興趣的朋友跟隨小編一起看看吧2023-04-04

