章节内容
历史介绍
开发环境
代码封装
函数式
获取小说命令行版(上)
获取小说命令行版(中)
获取小说命令行版(下)
嵌入到应用中
插件机制
文字转语音
设置与信号中断
插件商店
下载与删除(上)
下载与删除(下)
通知栏程序
播放功能
美化界面
数据可视化(上)
数据可视化(中)
数据可视化(下)
嵌入到应用中
打包应用

# 下载所有的内容

这里有一个并发的机制,用 while 和 Promise.all 其实就很好的控制。也可以使用 chunk (通过 reduce 、slice 可以非常简单的实现)函数分割成 4 个一组的二维数组,然后通过 Promise.all 执行。当然用 stream 编程的模式其实也可以控制,不过相对较难一些,原理就是限定并发量,当并发量超过的时候,通过延迟调用 done 方法控制下一个任务的进入。在我的个人网站有一期 stream 精讲有说明如何去做!并且还实现了一个简单的 RPC 调用。

spinner 是控制台的输出器,用来显示进度的,showNum 就是计算出来完成了多少的进度。

async function downAllText(spinner, crawl, opts) {
  const { path, concurrence, waitTime } = opts
  const chapters = require(resolve(path, 'chapters.json'))
  let index = 0
  while (index < chapters.length) {
    const currentTick = chapters.slice(index, index + concurrence) // 每次取 concurrence 个 数据构建 promise 通过 Promise.all 同时执行。
    await Promise.all(
      currentTick.map((chapter, i) => {
        return downloadText(chapter, crawl, index + i, opts)
      }) // 构建 promise 数组
    )
    // 等待事件
    await sleep(waitTime)
    let showNum = Math.floor(
      ((chapters.length - index) * 100) / chapters.length
    )
    // 设置终端文字
    spinner.color = 'red'
    spinner.text = '爬取中 ' + (100 - showNum) + '% -> ' + chapters[index].title
    index += concurrence
  }
}

# 最终的包装函数

在最后一定要使用 spinner.stop 让其停止,要不然有的时候会让控制台一直在转,最终的 download 会像下面的代码所示。

async function download(url, path, crawl) {
  const opts = Object.assign(
    {},
    {
      path,
      concurrence: 4,
      waitTime: 0,
      charset: 'utf-8'
    },
    crawl.opts
  ) // 设置默认初始选项,通过 assign 构建可被覆盖的默认值

  const spinner = ora('开始下载').start() // 提示信息开始旋转
  await ensureSavaPath(resolve(path, 'text')) // 确保文件存在
  spinner.color = 'yellow'
  spinner.text = '开始爬取章节' // 改变文字与颜色
  await downloadChapter(url, crawl, opts) // 下载章节
  spinner.color = 'blue'
  spinner.text = '开始爬取内容' // 改变文字与颜色
  await downAllText(spinner, crawl, opts) // 下载文章内容
  spinner.succeed(`爬取完成`) // 改变文字与停止
  spinner.stop()
}

# 准备一份爬取规则

这里使用正常的方式编程即可,用 function 的原因是为了保留 this 指向。 $ 是 cheerio 提供的选择器,它跟 jquery 的使用基本相同,具体请查阅 文档 。

// 笔趣阁爬取规则
const getDataFromYbdu = {
  opts: {
    charset: 'gbk' // 字符集,部分网站 gbk 编码,下载之后是乱码
  },
  chapter($, url) {
    // 章节获取规则
    const datas = []
    $('.mulu_list li').each(function(i, ele) {
      // 选择每一个连接
      const self = $(this)
      const title = self.text().replace(/\s/g, '') // 得到文本内容
      const chapter_url = self.find('a').attr('href') // 得到 dom 下面 a 标签 herf 属性
      datas.push({
        title, // 标题
        url: urljoin(url, chapter_url) // 内容页面的网址
      })
    })
    return datas
  },
  text($) {
    // 对内容页面的网址的爬取规则
    const trim = sourceString => {
      const dels = [
        '加入书签',
        '加入书架',
        '推荐投票',
        '返回书页',
        '上一页',
        '返回目录',
        '下一页',
        /\s+/gi,
        /(\-)*/gi
      ]
      dels.forEach(delString => {
        // 纯函数
        sourceString = sourceString.replace(delString, '') // 删除广告词
      })
      return sourceString
    }
    const sourceString = $('#htmlContent').text() // 获取内容
    return trim(sourceString)
  }
}

然后我们测试一下

download(
  'https://www.ybdu.com/xiaoshuo/0/910/', // 小说目录页
  resolve(__dirname, '../jstm'), // 保存地址
  getDataFromYbdu // 爬取规则
).catch(console.error)

# 读取命令行参数

process.argv 可以取到运行命令,去掉前两项是 node 跟 download.js ,网址末尾的 / 不能丢,丢了会有个 301 跳转,暂时还没做处理。

const args = process.argv.slice(2)
download(args[0], resolve(process.pwd(), args[0]), getDataFromYbdu).catch(
  console.error
)
node download.js https://www.ybdu.com/xiaoshuo/0/910/ ./jxtm

假如你需要更多参数解析功能,可以使用微型库 minimist ,假如你的是大型命令行程序,像 heroku 那样的,可以使用 oclif