來一條node爬蟲
阿新 • • 發佈:2020-12-22
用node寫個爬蟲真的肥腸煎蛋,今天就來玩一下。
物料
首先準備物料。
cnpm i axios
cnpm i cheerio
我們需要準備兩個第三方包,一個是axios,用來發送請求的,當然request包也行,看個人喜好了;另一個是cheerio,這貨是用來解析dom的,跟jquery的用法一樣一樣的。
爬蟲的實現思路
也就是說,我們通過axios請求過來的html的標籤資料,然後用cheerio包來提取我們需要的內容,緊接著我們可以用fs包的流來讀取資料,然後寫入我們的磁碟,這就是一個完整的爬蟲需要做的事情。
預熱
先預熱一下。
const axios = require('axios'); const cheerio = require('cheerio'); let httpUrl = 'http://www.adoutu.com/picture/list/1'; axios.get(httpUrl).then(res=>{ // console.log(res.data); //獲取資料 let $ = cheerio.load(res.data); //將dom節點匯入cheerio解析 $('.list-group .list-group-item a').each((i,ele)=>{ // 遍歷dom節點 let url = $(ele).attr('href'); //獲取a標籤的href屬性資訊 console.log(url); }) })
實現
預熱完畢,開始幹活。
const axios = require('axios'); const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); let httpUrl = 'http://www.adoutu.com/picture/list/1'; axios.get(httpUrl).then(res=>{ // console.log(res.data); //獲取資料 let $ = cheerio.load(res.data); $('.list-group .list-group-item a').each((i,ele)=>{ // 遍歷dom節點 let aUrl = $(ele).attr('href'); //獲取a標籤的href屬性資訊 parsePage('http://www.adoutu.com' + aUrl); }) }) async function parsePage(url) { let res = await axios.get(url); let $ = cheerio.load(res.data); let imgUrl = $('.detail-picture img').attr('src'); let urlObj = path.parse(imgUrl); let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`); axios.get(imgUrl,{responseType:'stream'}).then(res=>{ //指定獲取二進位制流 res.data.pipe(ws); res.data.on('close',()=>{ ws.close(); }) }) }
分頁
如果有分頁,我們可以先獲取分頁總數,然後迴圈發請求即可。
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
spider();
//迴圈發起請求
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
if(i < 5) // 如果page太大,會存在崩潰問題
getData(i)
else
break;
}
}
//獲取每頁資料
async function getData(page) {
let url = 'http://www.adoutu.com/picture/list/' + page;
console.log(url);
let res = await axios.get(url);
let $ = cheerio.load(res.data);
$('.list-group .list-group-item a').each((i, ele) => { // 遍歷dom節點
let aUrl = $(ele).attr('href'); //獲取a標籤的href屬性資訊
// console.log(aUrl);
parsePage('http://www.adoutu.com' + aUrl);
})
}
// 獲取分頁數
async function getNum() {
let res = await axios.get(httpUrl);
let $ = cheerio.load(res.data);
let count = $('.pagination li').length;
// console.log(count);
let pageNum = $('.pagination li').eq(count - 2).find('a').text();
// console.log(pageNum);
return pageNum;
}
//解析並存儲資料
async function parsePage(url) {
let res = await axios.get(url);
let $ = cheerio.load(res.data);
let imgUrl = $('.detail-picture img').attr('src');
let urlObj = path.parse(imgUrl);
let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`);
axios.get(imgUrl, { responseType: 'stream' }).then(res => {
res.data.pipe(ws);
res.data.on('close', () => {
ws.close();
})
})
}
延遲執行
上面我們發現了一個問題,就是分頁過多的情況下,程式就會報錯,為什麼呢?
因為我們一次性發送的請求太多了,for迴圈是同步執行的,刷的一下發那麼多請求,不掛才怪。怎麼辦呢?
這問題能難倒你嗎?讓他延遲執行唄。
不過請注意,這個延遲執行也沒那麼簡單,比如這麼寫:
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
setTimeout(()=>{
getData(i)
},2000);
}
}
...
這麼寫是不行的,因為這就等於你在事件佇列中建立了n個getData()函式,然後等待2s後執行這n個getData()函式,其實還是同時執行的。
那怎麼搞?
我們可以讓請求拉開距離執行:
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
setTimeout(()=>{
getData(i)
},2000*i);
}
}
這樣寫還是在事件佇列建立了n個getData(),不過他們的執行時間不一致了,分別是等待2s、4s…依次類推的執行,這樣就拉開了請求之間的距離。
這個等待場景還是蠻常用的哈,寫那麼多定時器還是很煩人的哈,我們可以將這個邏輯用promise封裝起來。
//等待
function sleep(time){
var timer;
return new Promise((resolve,reject)=>{
timer = setTimeout(()=>{
clearTimeout(timer);
resolve('請求延遲'+time)
},time)
})
}
然後我們利用這個等待函式就可以寫出比較優雅的程式碼了:
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
let httpUrl = 'http://www.adoutu.com/picture/list/1';
spider();
//迴圈發起請求
async function spider() {
let page = await getNum();
for (let i = 1; i <= page; i++) {
await sleep(2000*i) //每個請求等待2s、4s....後執行
getData(i)
}
}
//等待
function sleep(time){
var timer;
return new Promise((resolve,reject)=>{
timer = setTimeout(()=>{
clearTimeout(timer);
resolve('請求延遲'+time)
},time)
})
}
//獲取每頁資料
async function getData(page) {
let url = 'http://www.adoutu.com/picture/list/' + page;
console.log(url);
let res = await axios.get(url);
let $ = cheerio.load(res.data);
$('.list-group .list-group-item a').each(async (i, ele) => { // 遍歷dom節點
let aUrl = $(ele).attr('href'); //獲取a標籤的href屬性資訊
// console.log(aUrl);
await parsePage('http://www.adoutu.com' + aUrl);
})
}
// 獲取分頁數
async function getNum() {
let res = await axios.get(httpUrl);
let $ = cheerio.load(res.data);
let count = $('.pagination li').length;
// console.log(count);
let pageNum = $('.pagination li').eq(count - 2).find('a').text();
// console.log(pageNum);
return pageNum;
}
//解析並存儲資料
async function parsePage(url) {
let res = await axios.get(url);
let $ = cheerio.load(res.data);
let imgUrl = $('.detail-picture img').attr('src');
let urlObj = path.parse(imgUrl);
let ws = fs.createWriteStream(`./img/${urlObj['name']}${urlObj['ext']}`);
axios.get(imgUrl, { responseType: 'stream' }).then(res => {
res.data.pipe(ws);
res.data.on('close', () => {
ws.close();
})
})
}