Node-写个爬虫看本小说?

247 阅读3分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

前言

喜欢看辰东的小说,有段时间想看《圣墟》,但是找了一圈都没有 txt 资源,于是就萌生了一个自己想办法搞一个 txt 出来的想法。

仅作为学习使用,支持正版。

加一句,喜欢叶凡的霸气无匹,心疼石昊的独断万古。

需求拆解

需求很简单,就是找到一个可以在线看小说的平台,最好是免登陆就也可以看的,然后把看到的小说想办法保存到本地,并整理成一个 txt。

具体拆解:

  1. 本地启动一个 node 服务器,向目标地址发起请求
  2. 分析目标页面,解析 html 并保存文本
  3. 按照目录顺序,逐个下载小说文本
  4. 扩展: a- 提高效率,多本下载 . . . b- 抓取图片等其他素材

代码实现

代码分为两部分:

第一部分是启动一个服务,并基于服务请求目标 html

注意这个目标地址可能会变,需要根据实际情况进行修改。

第二部分是 解析 html

依赖说明

http/https: node模块用来发起请求

fs: node 文件模块

colors-console: 一个可以在控制台打印五颜六色信息的小工具,需要安装

path: node path模块

jsdom:可以将html文本解析成jQuery对象

具体代码如下:

// loadTxt.js

"use script"
const http = require('http')
const https = require('https')
const fs = require('fs')
const colors = require('colors-console')
const path = require('path')
const { JSDOM } = require('jsdom')

// 启动一个本地Node服务
const server = http.createServer((req, res) => { }).listen(50082)
console.log('http start at port 50082!')

const BaseHostname = 'https://www.xxxx.com/'

// 抓取目标地址
const options = {
  hostname: BaseHostname,
  path: '/book/23488/'
}

// 保存地址
const txtName = '圣墟-' + new Date().getTime() + '.txt'
const file = path.resolve(__dirname, '../txt/' + txtName)
// 变量记录递归的次数
let idx = 0

/**
 * 爬取网页内容,并返回经过 jquery 处理后端结果,可以直接执行 jquery 逻辑
 * @param {hostname, path as object} param0 
 * @returns jQuery Object by http result
 */
const html2jq = ({ hostname, path }) => {
  return new Promise((resolve, reject) => {
    let html = ''
    https.get(hostname + path, res => {
      res
        .on('data', chunk => {
          html += chunk.toString()

        })
        .on('end', () => {
          const document = (new JSDOM(html, { pretendToBeVisual: true })).window
          const $ = require('jquery')(document)
          return resolve($)
        })
        .on('error', (err) => {
          console.log(colors('red', err));
          return reject(err)
        })
    })
  })
}

/**
 * 递归下载章节,保证下载内容的准确性
 * @param {index} i
 * @param {data} arr 
 */
const saveByList = (i, arr) => {
  // console.log(`第${i}章下载开始!`)

  let options = {
    hostname: BaseHostname,
    path: arr[i].path
  }

  html2jq(options).then($ => {
    let title = arr[i].title
    let content = $("#content").text().split('    ').join('\r\n    ').replace(/    /g, '')
    fs.appendFile(file, title + '\r\n\r\n' + content + '\r\n\r\n\r\n', err => {
      if (err) console.log(err);
      server.close()
    })

    console.log(`第${i}${title} - 下载完成!`)

    idx++
    if (idx <= arr.length) {
      saveByList(idx, arr)
    }

  }).catch(err => {
    console.log('[IN SAVEBYLIST] ', err);
    server.close()
  })
}

/**
 * 执行逻辑
 * 1- 获取目录
 * 2- 根据目录递归获取章节内容
 */
html2jq(options).then($ => {
  let data = []

  $('#list dl dd').each(function () {
    data.push({
      path: $(this).find('a').attr('href'),
      title: $(this).find('a').text()
    })

  })
  saveByList(idx, data)
}).catch(err => {
  console.log(colors('red', `[ERROR==] ${err}`))
})

执行结果如下:

我把 options 里面的两个值改掉了,直接执行的话域名是错的,需要自己找到心仪的地址去试一下。

Animation.gif

后记

其实后来因为工作的原因也没看到大结局,听朋友说结局有点草率,唉!

这个工具只针对前后端不分离的项目有用,因为只有 服务端 渲染才会给一个完完整整的 html,现在前后端分离的项目这种思路就有点不好使了。但是也有其他的解决方案。

至于工具的用处除了炫技之外,貌似也没有啥实际用途了。