下载所有的内容

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

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

  1. async function downAllText(spinner, crawl, opts) {
  2. const { path, concurrence, waitTime } = opts
  3. const chapters = require(resolve(path, 'chapters.json'))
  4. let index = 0
  5. while (index < chapters.length) {
  6. const currentTick = chapters.slice(index, index + concurrence) // 每次取 concurrence 个 数据构建 promise 通过 Promise.all 同时执行。
  7. await Promise.all(
  8. currentTick.map((chapter, i) => {
  9. return downloadText(chapter, crawl, index + i, opts)
  10. }) // 构建 promise 数组
  11. )
  12. // 等待事件
  13. await sleep(waitTime)
  14. let showNum = Math.floor(
  15. ((chapters.length - index) * 100) / chapters.length
  16. )
  17. // 设置终端文字
  18. spinner.color = 'red'
  19. spinner.text = '爬取中 ' + (100 - showNum) + '% -> ' + chapters[index].title
  20. index += concurrence
  21. }
  22. }

最终的包装函数

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

  1. async function download(url, path, crawl) {
  2. const opts = Object.assign(
  3. {},
  4. {
  5. path,
  6. concurrence: 4,
  7. waitTime: 0,
  8. charset: 'utf-8'
  9. },
  10. crawl.opts
  11. ) // 设置默认初始选项,通过 assign 构建可被覆盖的默认值
  12. const spinner = ora('开始下载').start() // 提示信息开始旋转
  13. await ensureSavaPath(resolve(path, 'text')) // 确保文件存在
  14. spinner.color = 'yellow'
  15. spinner.text = '开始爬取章节' // 改变文字与颜色
  16. await downloadChapter(url, crawl, opts) // 下载章节
  17. spinner.color = 'blue'
  18. spinner.text = '开始爬取内容' // 改变文字与颜色
  19. await downAllText(spinner, crawl, opts) // 下载文章内容
  20. spinner.succeed(`爬取完成`) // 改变文字与停止
  21. spinner.stop()
  22. }

准备一份爬取规则

这里使用正常的方式编程即可,用 function 的原因是为了保留 this 指向。 $ 是 cheerio 提供的选择器,它跟 jquery 的使用基本相同,具体请查阅 文档获取小说命令行版(下) - 图1

  1. // 笔趣阁爬取规则
  2. const getDataFromYbdu = {
  3. opts: {
  4. charset: 'gbk' // 字符集,部分网站 gbk 编码,下载之后是乱码
  5. },
  6. chapter($, url) {
  7. // 章节获取规则
  8. const datas = []
  9. $('.mulu_list li').each(function(i, ele) {
  10. // 选择每一个连接
  11. const self = $(this)
  12. const title = self.text().replace(/\s/g, '') // 得到文本内容
  13. const chapter_url = self.find('a').attr('href') // 得到 dom 下面 a 标签 herf 属性
  14. datas.push({
  15. title, // 标题
  16. url: urljoin(url, chapter_url) // 内容页面的网址
  17. })
  18. })
  19. return datas
  20. },
  21. text($) {
  22. // 对内容页面的网址的爬取规则
  23. const trim = sourceString => {
  24. const dels = [
  25. '加入书签',
  26. '加入书架',
  27. '推荐投票',
  28. '返回书页',
  29. '上一页',
  30. '返回目录',
  31. '下一页',
  32. /\s+/gi,
  33. /(\-)*/gi
  34. ]
  35. dels.forEach(delString => {
  36. // 纯函数
  37. sourceString = sourceString.replace(delString, '') // 删除广告词
  38. })
  39. return sourceString
  40. }
  41. const sourceString = $('#htmlContent').text() // 获取内容
  42. return trim(sourceString)
  43. }
  44. }

然后我们测试一下

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

读取命令行参数

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

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

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