继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

精通 Node 爬虫 -03- 知乎专栏爬虫实战

布宝
关注TA
已关注
手记 22
粉丝 9581
获赞 319
精通 Node 爬虫 -03- 知乎专栏爬虫实战

2018 年 6 月 6 日 15:35:00

一年多前在慕课网手记里发布了篇 《nodejs 爬虫——知乎专栏》,并没有详细的说明这个爬虫的全部工作原理,只是贴了一份代码。经过了一年,原来的代码已经做了很多优化,并使用了知乎的 api 模块,后来发现 api 模块的作者很久没有维护导致API不可以,所以自己手动维护了知乎专栏的一小部分,不过并没PR(作者issues都不回复了,PR也没人理的)。

使用到的模块

由于知乎专栏现在使用的是API,抓取直接请求API就好,获取到数据,把数据转成markdown格式存在本地,方便以后看。所以使用的模块就三个。

"lodash"
"mkdirp"
"request"
"turndown"
抓取原理

在编写代码前,需要先对整个逻辑有个大概的思路:

  1. 使用request请求知乎专栏的数据
  2. 使用turndown把html内容转为markdown格式
  3. 保存为本地文件

原理是很简单,但是实际操作起来并没有这么容易。在写代码的过程中,就会发现各种各样的问题:

  1. 知乎专栏的api每次只能获取每次20条的数据。
  2. turndown处理代码块的标签嵌套和知乎API返回的不一样。
  3. 要是保存到一个并不存在的文件夹下,NodeJS的fs模块并不支持自动创建文件夹。

基于这几个问题,我们需要另外写对应的方法来解决问题。

  1. 编写对应的方法,输入知乎专栏的文章数量,返回一个包含所有文章URL的数组,遍历这个数组即可得到获取到所有的文章。
  2. 使用正则把代码块的标签嵌套替换成turndown能识别的格式,方便得到我们想要的MarkDown格式的文档。
  3. 文件路径不存在就创建路径。

经过合理的封装,最顶部的代码就5行:

async function zhuanlan(postID, localPath = './') {
    console.log(`----- ${postID} start -----`);
    mkdir(path.resolve(localPath, postID));
    markdown(localPath, postID, await Posts(postID));
};

实际上正真起作用的代码就两行,创建文件夹,将专栏API返回的所有数据转成markdown,并储存。

代码解析

上面的代码可以分成三个大模块,分别是mkdirPostsMarkDown

mkdir 创建文件夹

因为fs的mkdir方法只能创建一层的文件夹,如果想创建更深层的文件夹,还是使用已经封装好的mkdirp模块。

function mkdir(filePath) {
    if (fs.existsSync(`${filePath}`)) {
        console.log(`  ${path.basename(filePath)} 文件夹已经存在`);
    } else {
        mkdirp(`${filePath}`, (err) => {
            if (err) {
                console.error(err);
            } else {
                console.log(`? 创建 ${path.basename(filePath)}文件夹成功`);
            }
        });
    }
}

文件夹是存放markdown文档的基础,只有先创建好文件夹,才能保证爬虫抓取的数据有用处。要不然爬了也是白爬取。

Posts 抓取逻辑

为什么这个方法叫Posts,因为知乎的专栏文章的API就使用这个为路径。

在这个方法里,实现了将所有的文章API的URL处理成数组,并循环抓取所有的文章数据。

获取专栏的信息,专栏信息中有专栏文章的数量值,需要先获取到这个信息才能知道循环请求何时停止。使用lodash的模板方法,得到url,因为返回的数据是压缩过的,需要request解压,所以要设置gzip: true

const zhuanlanInfo = async (columnsID) => {
    const urlTemplate = _.template(API.post.columns)({ columnsID });
    let object = {};
    object = {
        url: urlTemplate,
        gzip: true,
    };
    return JSON.parse((await request(object)).body);
}

将文章数量转换为循环次数。每次访问20条数据,减少请求的数量,节省时间的同时,还能减少对知乎服务器的负担,没毛病。(这里没写延时函数,如果持续抓取,建议加个延时函数)。

let rateMethod = (count, cycle) => {
    count = count === undefined ? 20 : count;
    cycle = cycleMethod(cycle);
    let posts = count % cycle;
    let times = (count - posts) / cycle;
    return {
        times,
        count,
        cycle,
        writeTimes: 0,
        allObject: {}
    }
}

let cycleMethod = (cycle) => {
    let defaultCycle = 20;
    if (cycle && cycle !== defaultCycle) {
        cycle = cycle % defaultCycle;
    }
    cycle = cycle || defaultCycle;
    return cycle;
}

这里得到的循环次数是从0开始计算的。

循环函数获取所有文章

let loopMethod = (config, callback) => {
    let { urlTemplate, ...options } = config.options;
    let opts = {
        url: url.resolve(urlTemplate, `?limit=${config.cycle}&offset=${config.writeTimes * 20}`),
        ...options
    }
    request(opts).then((c) => {
        c =  JSON.parse(c.body);
        forEach(c, (item, index) => {
            config.allObject[index + config.writeTimes * 20] = item;
        });
        if (config.writeTimes === config.times) {
            callback(config.allObject);
        } else {
            config.writeTimes += 1;
            loopMethod(config, callback);
        }
    });
}

这里使用的是callback,当然也可以写成Promise,谁写成Promise记得给我PR一下哈。

抓取过程到这里就完了。下面是数据处理的工作了。

MarkDown 数据处理与储存

获取到数据,最后需要保存成MarkDown的格式。这个模块处理的事情就是将所有的数据转换成MarkDown语法格式,保存到本地。

这一块的代码还是存在一些bug,在windows系统下,文件命名不能存在特殊字符,有心的朋友可以给我PR修复下问题哦。具体怎么写的可以看看我的代码仓库,这里就不多说了。

食用方法

emmmmmm......自己看README吧。

打开App,阅读手记
3人推荐
发表评论
随时随地看视频慕课网APP