1. 程式人生 > 其它 >來一條node爬蟲

來一條node爬蟲

技術標籤:node爬蟲jsnodejsnode.js

用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();
        })
    })
}