如何用axios和cheerio写爬虫

4,024 阅读5分钟

js爬虫

最近想写一个爬虫脚本,查了查网上的脚本教程都是基于pyhton的比较多,而js就很少。问了问我的小伙伴js爬虫需要什么工具吗?大概需要的工具有axios,cheerio,当然也安装了node.js。

安装好了基本工具之后,我们就开始快乐coding了,axios是用来前后端交互比较多,这里我用来获取html,想到之前爬豆瓣的时候遇到ajax,页面元素并不是在渲染好了就有数据,导致拿不到想要的信息。

在网上看到了D版图书的网址,一看就是不会对我的爬虫有什么抵御措施,也正好练手。

首先第一步,我们要拿到html,打开网页分析一下页面元素,在这里我访问的链接是http://www.shuquge.com/txt/108449,耳根大大的书,并且寻找到小说章节的url是在.listmain dl dd a下

下面是我的代码

let instance = axios.create({
    baseURL: 'http://www.shuquge.com/txt/108449',
    timeout: 2500,
});

//得到html模板
async function getHtml() {
    let html = await instance.get('index.html').then(res => res.data);
    return getUrl(html);
}

//得到书本章节的url请求
function getUrl(html) {
    let $ = cheerio.load(html);
    let list = Array.from($('.listmain dl dd a'));
    let urlArr = [];
    // ele.attribs.href
    list.forEach(ele => urlArr.push(ele.attribs.href));
    return urlArr;
}

这两个函数执行后,返回的是这本小说的所有章节的url,这样我就离最后爬完整本书做了基础,剩下的就是访问这些url,获得具体小说信息,并且保存下来就好了。

function sendGetRequest(url) {
    return instance.get(url);
}

async function getNovel() {
    let urlArr = await getHtml();
    fetchUrl(10, urlArr).then(res => {
        console.log('结果为' + res.length);
        for (let i = 0; i < res.length; i++) {
            console.log(res[i]);
            fs.appendFile('三寸人间.txt', res[i], function () {
                (console.log('写入第' + i + '章' + '成功'))
            })
        }
    })
}

这里的代码我已经修改过,原来的想法是,小说的抓取是顺序一章章抓取,那么写文件的时候的结果也是一章章。写个for循环,简单又能实现抓取小说的目的。

但是怎么能这么简单就结束呢?我的小伙伴和我说,搞个并发,一次发起n次请求效率比原来快得多。我上网查阅了这方面的资料,实在是很少。我设定了一个max值,然后发送请求就max++,如果到达n就不在发送新的。

这样想会出现几个问题,一、我的请求完成获取不到二、只能并发一次,达到上限之后就不会继续执行。

我开始想的很单纯,既然这样我就外面写个async 里面写个await 等待并发的请求结束,那其实就是和同步执行并无差别,只是等待一个批次,一个批次有n个请求, 等待这一个批次的请求都返回了,才会执行下一个批次的请求。 我对这样的结果并不满意。

接下来是我优化后的代码。

//howmany是一次抓取多少,urlArr是url数组
async function fetchUrl(howMany, urlArr) {
    return new Promise((res, rej) => {
        let result = [];//保存爬取记录
        let count = 0;//记录url在数组中的坐标
        let task = [];//任务队列
        let remain = 0;//记录剩余未抓取的url
        let doTask = (indexOfTasks, indexOfUrls = count) => {
            //生成promise,并且执行
            let req = sendGetRequest(urlArr[indexOfUrls]);
            req.then(data => {
                remain--;
                if (count >= urlArr.length) {
                    if (remain === 0) return res(result);
                    return;
                }
                doTask(indexOfTasks);
                result[indexOfUrls] = getContent(data.data);
                count++;
                remain++;
            }, rej => {
                console.log("请求失败" + rej);
                doTask(indexOfTasks, indexOfUrls);
            });
            console.log('正在发送第' + count + '次请求' + '并发数为' + task.length);
            task[indexOfTasks] = req;
        }

        for (let i = 0; i < howMany; i++) {
            doTask(i);
            count++;
            remain++;
        }
    })
}

这里的内容就比较多了,先解释一下这个代码块。

for (let i = 0; i < howMany; i++) {
    doTask(i);
    count++;
    remain++;
}

执行的时候发送howmany个请求,并记录发送的请求数,remain记录队列里还有多少未完成的请求。

比如我先发送10个请求,就相当于是叫10个工人干活,工人完成了自己的工作后,就让其他工人顶替自己的位置,而工位就只有10个。

工人完成自己的任务之后,吩咐自己的同事接下来要做什么样的工作,并且让同事坐到自己的工位上。

当工厂的剩余任务为0(remain等于零的时候) 工人就可以下班了。

但是因为是并发,所以你不知道哪个工人先完成任务,即小说的章节是乱序的,为此我又用闭包记录了当前url在数组中的位置,这样虽然是并发执行的,但是结果是按位置加入数组的。

解决了这个问题之后,我又在想万一出错了怎么办就用这个坐标值,拿到出错的url,重新获取小说内容。

爬取多部小说

完成了这个任务之后,我就在思考,如果每次都需要知道url地址,从而爬取某部小说的内容,这个脚本还不够智能。 于是我分析了首页小说的html,获取到每个版块榜首的小说url和名字,再通过这个url进行之前的操作,那不是就可以获取到这个小说网版块榜首的小说,但不用自己再去看Html。 说干就干,最后的代码是这样。

const cheerio = require('cheerio');
const axios = require('axios');
const fs = require('fs');


//获取首页热门小说url,并创建axios
async function getHotNovelUrl() {
    let novel = {
        Names: null,
        Urls: null
    };
    let instance = axios.create({
        baseURL: 'http://www.shuquge.com/',
        timeout: 2500,
    });
    let result = await instance.get().then(res => res.data);
    let $ = cheerio.load(result);
    novel.Names = Array.from($('.block .image a img')).map(ele => ele.attribs.alt);
    novel.Urls = Array.from($('.top dl dt a')).map(ele => 'txt/' + ele.attribs.href.match(/[0-9]/g).join('') + '/');
    console.log(novel);
    return novel;
}

//得到html模板,index代表axiosArr中的位置
async function getHtml(url) {
    let instance = axios.create({
        baseURL: 'http://www.shuquge.com/' + url,
        timeout: 2500
    })
    let html = await instance.get('index.html').then(res => res.data);
    return getUrl(html);
}

//得到书本章节的url请求
function getUrl(html) {
    let $ = cheerio.load(html);
    // let document.querySelectorAll(".listmain dl dd a")[0].getAttribute("href");
    let list = Array.from($('.listmain dl dd a'));
    let urlArr = [];
    list.forEach(ele => urlArr.push(ele.attribs.href));
    return urlArr;
}

function getContent(html) {
    let $ = cheerio.load(html);
    let content = $('.content h1').text() + $('#content').text();
    return content;
}

function sendGetRequest(baseURL, url) {
    console.log(baseURL, url);
    let instance = axios.create({
        baseURL: 'http://www.shuquge.com/'+ baseURL,
        timeout: 2500
    })
    return instance.get(url);
}

async function getNovel() {
    let novels = await getHotNovelUrl();
    let urlArr = [];
    for (let i = 0; i < novels.Urls.length; i++) {
        await getHtml(novels.Urls[i]).then(result => urlArr.push(result));
    }
    console.log(urlArr);
    for (let i = 0; i < urlArr.length; i++) {
        await fetchUrl(50, urlArr[i], novels.Urls[i]).then(res => {
            console.log('结果为' + res.length);
            for (let j = 0; j < res.length; j++) {
                console.log(res[j]);
                fs.appendFile(novels.Names[i]+'.txt', res[j], function () {
                    (console.log('写入第' + j + '章' + '成功'))
                })
            }
        })
    }
}


//howmany是一次抓取多少,urlArr是url数组
async function fetchUrl(howMany, urlArr, baseURL) {
    return new Promise((res, rej) => {
        let result = []; //保存爬取记录
        let count = 0; //记录url在数组中的坐标
        let task = []; //任务队列
        let remain = 0; //记录剩余未抓取的url
        let doTask = (indexOfTasks, indexOfUrls = count) => {
            //生成promise,并且执行
            let req = sendGetRequest(baseURL, urlArr[indexOfUrls]);
            req.then(data => {
                remain--;
                if (count >= urlArr.length) {
                    if (remain === 0) return res(result);
                    return;
                }
                doTask(indexOfTasks);
                result[indexOfUrls] = getContent(data.data);
                count++;
                remain++;
            }, rej => {
                console.log("请求失败" + rej);
                doTask(indexOfTasks, indexOfUrls);
            });
            console.log('正在发送第' + count + '次请求' + '并发数为' + task.length);
            task[indexOfTasks] = req;
        }

        for (let i = 0; i < howMany; i++) {
            doTask(i);
            count++;
            remain++;
        }
    })
}


getNovel();

希望能给大家带来帮助,另外这段代码仅为学习之用,不要给人家带来不必要的服务器负担。