来一条node爬虫

作者: 姜治宇 | 来源:发表于2020-12-19 13:35 被阅读0次

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

    相关文章

      网友评论

        本文标题:来一条node爬虫

        本文链接:https://www.haomeiwen.com/subject/ubiqnktx.html