likes
comments
collection
share

趣味NodeJS之笔趣阁的小蜘蛛

作者站长头像
站长
· 阅读数 26

「本文正在参与技术专题征文Node.js进阶之路,点击查看详情

作为一个可以让JavaScript运行在服务端的平台,近年来NodeJS不可谓不火,于是乎前端崽们多少都会一些。今儿,我就在学习之中分享一个NodeJS写的抓取笔趣阁小说的爬虫。

前言

因为个人比较喜欢看小说,所以笔趣阁就成了我看小说的一个重要途径。虽然笔趣阁是免费的一个平台,但是并没有提供下载小说的方式。 所以我就在想是否可以通过我所学的NodeJS知识来爬取上面的小说,所以就有下面的内容了。

相关技术

除了需要掌握NodeJS以外,还需要借助第三方库来帮助编码,毕竟站在巨人上才能看的更远。这里,我选择的是 axioscheerio 两个库。

  • aixos : 这个库我就不必介绍了,写前端的都懂;
  • cheerio : 这个库简单一句话介绍就是服务端的jQuery

为什么没有用比较新的 puppeteer 库? 这个库网上都比较推荐,能比较方便的爬取异步渲染的数据。但我使用puppeteer开发爬虫发现一个问题:当请求的页面过多时,占用的内存会变很多。即使一个简单的小说网站也有上千个请求,所以需要一定的优化方式,否则爬取速度会很缓慢。

思路

笔趣阁作为一个服务端渲染的网站,所以使用axios+cheerio也能非常简单的爬取了。

  1. 找到目标小说的url,解析该小说的书名和目录(目录有对应章节的链接);
  2. 遍历小说目录,通过请求对应的path,来解析出文章内容;
  3. 把解析的内容通过fs模块写到文件里。

有了一个简单的思路,就可以开始编码实现了。

实现

  1. 创建一个index.js文件,并通过npm init -y初始化,然后下载我们要用的依赖:
npm install --save axios cheerio
  1. 请求目标小说,获取内容:
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

趣味NodeJS之笔趣阁的小蜘蛛

从上图,可以看到,通过axios直接请求地址后,会获得返回的HTML内容,有了这个内容后,就可以通过cheerio来获取任意的内容。

  1. 分析节点信息,使用cheerio获取需要的信息:

趣味NodeJS之笔趣阁的小蜘蛛

找到有用的节点信息后,开始编码:

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)

趣味NodeJS之笔趣阁的小蜘蛛

从上面可以看到cheerioAPIjQuery基本一样方便,因此也很容易的获取到了目录的信息。有了目录的章节信息后,就可以依葫芦画瓢,遍历请求抓取对应内容了。 现在,能抓取到内容后,就可以考虑下一步:把抓取的文章内容保存到本。

  1. 保存文章内容到本地文件: 在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()

趣味NodeJS之笔趣阁的小蜘蛛

正常情况下,就可以看到dist目录下生成的对应书名的.txt文件了,这样小说就算简单的爬取下来了。

优化

虽然实现了下载小说的目的,但发现了有几个比较严重的问题所在:

  1. 小说章节太多,下载缓慢;
  2. 请求过于频繁,有些请求会失败,造成内容补全;
  3. 长时间的程序执行,没有反馈交互,显得程序像卡死。

发现问题所在后,就得想办法来解决,思考之后想出以下方式:

  1. 通过setTimeout来开启异步,加快下载速率;
  2. 使用try/catch配合递归的方式,一直等请求成功(可以设置一个重试次数);
  3. 使用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.

完成

优化完成后,就可以看看现在的效果了:

趣味NodeJS之笔趣阁的小蜘蛛

趣味NodeJS之笔趣阁的小蜘蛛

当下载完成之后,就会把这几个文件归档为一个斗破苍穹.txt的文件。

以上,就是怎样使用NodeJS抓取一个普通网站内容的思路了,谢谢阅读Thanks♪(・ω・)ノ。