扒拉小说摸鱼

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

一、背景

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

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

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

二、目标站点

示例站点为: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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容