扒拉小说摸鱼

申明:上班摸鱼不好,扒拉小说也不对,不管你们信不信,本项目只为技术练习

一、背景

年后刚开工,比较无聊,想看小说但是又觉得太光明正大,那能不能把小说放到编辑器里面看呢。

找了好多平台都没有我需要的小说下载,于是决定自己写一个爬虫,去扒拉

扒拉简单,但是很多网站出来的都不是存文本内容,所以决定自己写一段代码转换成自己想要的格式,输出成文件

二、目标站点

示例站点为:https://www.mht99.com

三、需求

  • 能获取大部分免费网站的内容
  • 能根据目标网站的文章规律自动加载下一章
  • 能输出成自己想要的格式和文件类型

四、准备

1、创建一个 nodejs 项目

npm init

全部默认配置就好了,或者也可以详细填写

2、构建目录结构

  • 入口文件 index.js
  • 业务文件夹 src
  • 接口文件夹 apis
  • 文件存储文件夹 assets

3、依赖

功能简单,用不到依赖,不过这里安装了一个 request 来调取接口,也可以用原声的 http/https,但是考虑到各网站的区别,选择 request 比较方便。

五、业务逻辑

1、测试 api 是否能获取目标文章的内容——api.js

const request = require('request')

function getPage(uri) {
  return new Promise((resolve, reject) => {
    request(uri, (err, res, body) => {
      if (err) {
        console.error('api -------', 'error: ', err)
        resolve(0)
      } else if (res.statusCode === 200) {
        console.log('api -------', 'body: ', body)
        resolve(res)
      } else {
        resolve(0)
        console.log('api -------', 'code: ', res.statusCode)
      }
    })
  })
}

module.exports = {
  getPage
}

运行下这段代码,发现能拿到目标网页。因为目标网站的文章内容不是来源于接口,只能获取整个页面。

2、内容处理——src/write

  • 根据内容提取正文
  • 将每一章放在一个文件或多章放在一个文件中
  • 网络错误,链接错误,文章内容结束后断开请求
const fs = require('fs')
const apis = require('./api')

/**
 * @param startId                       第一章id
 * @param fileName                      文件名
 * @param chapterSliceNumber            每隔多少章节分割一次文件
 * @param endNumber                     请求多少章节停止
 * @param continuousErrorCloseNumber    连续错误关闭(次数)
 */

class WriteChapter {
  #url = 'https://www.mht99.com/17023' // 网站
  #pageIndex = 0 // 页码
  #fileIndex = 1 // 文件下标
  #finishChapterNumber = 0 // 已完成加载的数量
  #data = [] // 数据存放
  #continuousErrorNumber = 0 // 连续请求错误次数
  reg = /<div id="content">[\s\S]*10000/

  constructor(startId, fileName = '文章', chapterSliceNumber = 100, endNumber = 10000, continuousErrorCloseNumber = 5) {
    if (this.#aguTypeValidate(startId, endNumber, chapterSliceNumber, continuousErrorCloseNumber, fileName)) {
      this.chapterSliceNumber = chapterSliceNumber
      this.startId = startId
      this.fileName = fileName
      this.endNumber = endNumber
      this.continuousErrorCloseNumber = continuousErrorCloseNumber
    } else {
      console.error('错误:传入参数类型错误!\n 示例:new WriteChapter(13111, "西游记", 100, 1000, 5)')
    }
  }

  start() {
    this.#getPage().then(() => {
      this.start()
    })
  }

  #getPage() {
    return new Promise((resolve, reject) => {
      if (this.startId) {
        let id = this.#pageIndex === 0 ? this.startId : `${this.startId}_${this.#pageIndex}`

        apis.getPage(`${this.#url}/${id}.html`).then((res) => {
          const body = res.body

          if (body === 0) {
            this.#pageIndex = 0
            this.startId++
            this.#continuousErrorNumber++

            if (this.#continuousErrorNumber >= this.continuousErrorCloseNumber) {
              reject()
            } else {
              resolve()
            }
          } else {
            if (this.#finishChapterNumber >= this.endNumber) {
              this.#pushData(body)
              this.#write()
              this.#data = []
              reject()
              return
            }

            if (this.#finishChapterNumber !== 0 && this.#finishChapterNumber % this.chapterSliceNumber === 0) {
              this.#pushData(body)
              this.#write()
              this.#data = []
              this.#pageIndex = 0
              this.startId++
              this.#finishChapterNumber++
              this.#fileIndex++
              resolve()
            } else {
              this.#pushData(body)
              this.#finishChapterNumber++
              this.#pageIndex++
              resolve()
            }
          }
        })
      }
    })
  }

  #pushData(res) {
    let str = this.reg.exec(res) && this.reg.exec(res)[0] ? this.reg.exec(res)[0] : ''

    if (this.#data && this.#data[0] && str === this.#data[this.#data.length - 1]) {
      this.#data.push(str)
      this.#finishChapterNumber++
      this.#pageIndex = 0
      this.startId++
      this.#continuousErrorNumber++
    } else {
      this.#continuousErrorNumber = 0
      this.#data.push(str)
    }
  }

  #write() {
    let fileFullName = `./assets/${this.fileName}_${this.#fileIndex}_${this.startId}.json`
    let data = this.#data
    let html = ''
    data.forEach((item) => {
      item = item
        .replace('<div id="content">', '')
        .replace('<p data-id="10000', '<br/>')
        .replace(/[,。?!]/g, '<br/>')
      html += item
    })
    let json = html.split('<br/>')
    fs.writeFile(fileFullName, JSON.stringify(json), (err) => {
      if (err) {
        console.error('-------', 'err: ', err)
      } else {
        console.log('-------', 'success: ', fileFullName)
      }
    })
  }

  #aguTypeValidate(startId, endNumber, chapterSliceNumber, continuousErrorCloseNumber, fileName) {
    let startIdV = !isNaN(Number(startId)),
      chapterSliceNumberV = !chapterSliceNumber || !isNaN(Number(chapterSliceNumber)),
      endNumberV = !endNumber || !isNaN(Number(endNumber)),
      continuousErrorCloseNumberV = !continuousErrorCloseNumber || !isNaN(Number(continuousErrorCloseNumber)),
      fileNameV = typeof fileName === 'string'

    return startIdV && chapterSliceNumberV && endNumberV && continuousErrorCloseNumberV && fileNameV
  }
}

module.exports = WriteChapter

这里将所有方法封装到了一个类中,如果是同一个站点,可以直接使用。输出内容为 json 文件,每一个标点符号分割成一行,可以修改#write 方法,将输出内容改为自己需要的文件类型及各式。当然,最好是多设置几个各式,可以根据参数选择。

3、入口文件

const WriteChapter = require('./write')

const startId = 35254397
const fileName = '技能'

const writeChapter = new WriteChapter(startId, fileName, 20, 1000)

writeChapter.start()

引入 src/write 中的累,传入参数,开始扒拉

六、扒拉完本

1、查看小说第一章的 id

2、查看小说最后一章的 id

3、计算下整书大概有多少章节,每章大概多少页,可以计算出调用次数

4、防止中间断开后需要继续,输出的文件名可以设置为最后一次成功的 id

七、讲的好乱,其实没啥东西,直接上链接好了

码云仓库地址:https://gitee.com/webxingjie/get-novel/tree/master

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容