趣味NodeJS之笔趣阁的小蜘蛛
「本文正在参与技术专题征文Node.js进阶之路,点击查看详情」
作为一个可以让JavaScript运行在服务端的平台,近年来NodeJS不可谓不火,于是乎前端崽们多少都会一些。今儿,我就在学习之中分享一个NodeJS写的抓取笔趣阁小说的爬虫。
前言
因为个人比较喜欢看小说,所以笔趣阁就成了我看小说的一个重要途径。虽然笔趣阁是免费的一个平台,但是并没有提供下载小说的方式。
所以我就在想是否可以通过我所学的NodeJS
知识来爬取上面的小说,所以就有下面的内容了。
相关技术
除了需要掌握NodeJS
以外,还需要借助第三方库来帮助编码,毕竟站在巨人上才能看的更远。这里,我选择的是 axios 和 cheerio 两个库。
aixos
: 这个库我就不必介绍了,写前端的都懂;cheerio
: 这个库简单一句话介绍就是服务端的jQuery
。
为什么没有用比较新的 puppeteer 库? 这个库网上都比较推荐,能比较方便的爬取异步渲染的数据。但我使用
puppeteer
开发爬虫发现一个问题:当请求的页面过多时,占用的内存会变很多。即使一个简单的小说网站也有上千个请求,所以需要一定的优化方式,否则爬取速度会很缓慢。
思路
笔趣阁作为一个服务端渲染的网站,所以使用axios
+cheerio
也能非常简单的爬取了。
- 找到目标小说的
url
,解析该小说的书名和目录(目录有对应章节的链接); - 遍历小说目录,通过请求对应的
path
,来解析出文章内容; - 把解析的内容通过
fs
模块写到文件里。
有了一个简单的思路,就可以开始编码实现了。
实现
- 创建一个
index.js
文件,并通过npm init -y
初始化,然后下载我们要用的依赖:
npm install --save axios cheerio
- 请求目标小说,获取内容:
const Axios = require('axios')
const cheerio = require('cheerio')
const baseURL = 'https://www.xbiquge.la/' // 笔趣阁的网站
const outDir = 'dist' // 写入的文件目录
const targetURL = '7/7877/' // 通过网站url的规律,找到对应的书名pathname
const axios = Axios.create({
timeout: 0
})
const res = await axios.get(baseURL + targetURL)
console.log(res.data
从上图,可以看到,通过axios
直接请求地址后,会获得返回的HTML
内容,有了这个内容后,就可以通过cheerio
来获取任意的内容。
- 分析节点信息,使用
cheerio
获取需要的信息:
找到有用的节点信息后,开始编码:
const res = await axios.get(baseURL + targetURL)
const $ = cheerio.load(res.data)
const title = $('#info h1').text()
const list = []
$('#list a').each((idx, item) => {
list.push({
index: idx,
url: item.attribs.href,
title: item.children[0].data
})
})
console.log('书籍名称:', title)
console.log('书籍目录:', list)
从上面可以看到cheerio
的API
和jQuery
基本一样方便,因此也很容易的获取到了目录的信息。有了目录的章节信息后,就可以依葫芦画瓢,遍历请求抓取对应内容了。
现在,能抓取到内容后,就可以考虑下一步:把抓取的文章内容保存到本。
- 保存文章内容到本地文件:
在
NodeJS
中,可以通过 fs 模块来读写文件,使用起来非常的容易。
const ws = fs.createWriteStream(resolve(outDir, `${title}.txt`), {
flags: 'a' // 表示写入方式为追加
})
for (const b of list) {
const content = '...' // 通过cheerio分别获取内容
ws.write(title + '\n') // \n换行符
ws.write(content + '\n')
}
ws.end()
正常情况下,就可以看到dist
目录下生成的对应书名的.txt
文件了,这样小说就算简单的爬取下来了。
优化
虽然实现了下载小说的目的,但发现了有几个比较严重的问题所在:
- 小说章节太多,下载缓慢;
- 请求过于频繁,有些请求会失败,造成内容补全;
- 长时间的程序执行,没有反馈交互,显得程序像卡死。
发现问题所在后,就得想办法来解决,思考之后想出以下方式:
- 通过
setTimeout
来开启异步,加快下载速率; - 使用
try/catch
配合递归的方式,一直等请求成功(可以设置一个重试次数); - 使用
cli-progress
来反馈下载进度。
分片下载
通过setTimeout
来异步下载小说,需要考虑顺序问题。这里定义一个step
来表示分成几段来下载,最后来把片段给接上:
async function downloadBook (title, book, p = 5) {
let c = 0 // 下载完成的片段数量
const d = {
book: [],
ws: []
}
if (!isExist(outDir)) {
fs.mkdirSync(outDir)
} else {
fs.readdirSync(outDir).forEach(file => {
if (!fs.statSync(pathFor(file)).isDirectory()) {
fs.unlinkSync(pathFor(file))
}
})
}
for (let i = 0 ; i < p ; i ++) {
const step = Math.ceil(book.length / p)
d.book.push(i === p - 1 ? book.slice(i * step) : book.slice(i * step, (i + 1) * step))
d.ws.push(fs.createWriteStream(pathFor(`${title + i}.txt`), {
flags: 'a'
}))
d.ws[i].on('finish', async () => {
c ++
if (c === p) {
console.log('\n下载完成,文件归档中...')
for (let i = 0 ; i < p ; i ++) {
const target = fs.createWriteStream(pathFor(`/${title}.txt`), {
flags: 'a'
})
const rs = fs.createReadStream(pathFor(`${title + i}.txt`))
await pipeSync(rs, target)
}
console.log('下载完成!')
process.exit()
}
})
}
console.log(`开始下载${title}了:`)
const bar = new cliProgress.SingleBar({
format: '下载进度:[{bar}] {percentage}% | 预估时间: {eta}s | {value}/{total}',
barIncompleteChar: '-'
}, cliProgress.Presets.rect)
bar.start(book.length, 0)
for (let i = 0 ; i < p ; i ++) {
setTimeout(async () => {
for (const b of d.book[i]) {
const { $c } = await getHTMLContent(baseURL + b.url, {
$c: '#content'
}, b.title)
const content = $c.text()
d.ws[i].write(b.title + '\n')
d.ws[i].write(content + '\n')
bar.increment()
}
d.ws[i].end()
})
}
}
以上就是分段来异步下载的一个简单实现,接着看怎么处理异常:
异常处理
请求的次数多了后,肯定会出现丢包的现象,从而导致内容不全,这种时候就需要做对应处理了:
async function getHTMLContent (url, selectors, title = '') {
try {
const result = {}
const res = await axios.get(url)
const $ = cheerio.load(res.data)
for (const k in selectors) {
result[k] = $(selectors[k])
}
return result
} catch(err) {
console.log('\n下载' + (title || url) + '出现网络错误,将重新尝试...')
return getHTMLContent(url, selectors)
}
}
当因为网络失败后,会try
捕获走catch
方法,在catch
方法里我们递归调用,从新再次请求这次失败的章节,保证内容不会丢失。
进度条反馈
使用进度条的话,需要下载 cli-progress 库:
npm install --save cli-progress
使用方式非常的简单:
const bar = new cliProgress.SingleBar({
format: '下载进度:[{bar}] {percentage}% | 预估时间: {eta}s | {value}/{total}',
barIncompleteChar: '-'
}, cliProgress.Presets.rect)
bar.start(200, 0) // 初始化总数,当前数
bar.increment() // +1
其中的几个参数表达意思为:
format
: 表示格式化后的显示内容;barIncompleteChar
: 未下载的显示字符;start
: 初始化进度条,第一个参数是总是,第二个是完成数:一般为0;increment
: 完成数自增1.
完成
优化完成后,就可以看看现在的效果了:
当下载完成之后,就会把这几个文件归档为一个斗破苍穹.txt
的文件。
以上,就是怎样使用NodeJS抓取一个普通网站内容的思路了,谢谢阅读Thanks♪(・ω・)ノ。
转载自:https://juejin.cn/post/7072716370621759495