扒拉小說摸魚

申明:上班摸魚不好,扒拉小說也不對(duì)畅涂,不管你們信不信港华,本項(xiàng)目只為技術(shù)練習(xí)

一、背景

年后剛開工午衰,比較無聊立宜,想看小說但是又覺得太光明正大,那能不能把小說放到編輯器里面看呢臊岸。

找了好多平臺(tái)都沒有我需要的小說下載橙数,于是決定自己寫一個(gè)爬蟲,去扒拉

扒拉簡(jiǎn)單帅戒,但是很多網(wǎng)站出來的都不是存文本內(nèi)容灯帮,所以決定自己寫一段代碼轉(zhuǎn)換成自己想要的格式,輸出成文件

二蜘澜、目標(biāo)站點(diǎn)

示例站點(diǎn)為:https://www.mht99.com

三施流、需求

  • 能獲取大部分免費(fèi)網(wǎng)站的內(nèi)容
  • 能根據(jù)目標(biāo)網(wǎng)站的文章規(guī)律自動(dòng)加載下一章
  • 能輸出成自己想要的格式和文件類型

四响疚、準(zhǔn)備

1鄙信、創(chuàng)建一個(gè) nodejs 項(xiàng)目

npm init

全部默認(rèn)配置就好了,或者也可以詳細(xì)填寫

2忿晕、構(gòu)建目錄結(jié)構(gòu)

  • 入口文件 index.js
  • 業(yè)務(wù)文件夾 src
  • 接口文件夾 apis
  • 文件存儲(chǔ)文件夾 assets

3装诡、依賴

功能簡(jiǎn)單,用不到依賴践盼,不過這里安裝了一個(gè) request 來調(diào)取接口鸦采,也可以用原聲的 http/https,但是考慮到各網(wǎng)站的區(qū)別咕幻,選擇 request 比較方便渔伯。

五、業(yè)務(wù)邏輯

1肄程、測(cè)試 api 是否能獲取目標(biāo)文章的內(nèi)容——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
}

運(yùn)行下這段代碼锣吼,發(fā)現(xiàn)能拿到目標(biāo)網(wǎng)頁(yè)选浑。因?yàn)槟繕?biāo)網(wǎng)站的文章內(nèi)容不是來源于接口,只能獲取整個(gè)頁(yè)面玄叠。

2古徒、內(nèi)容處理——src/write

  • 根據(jù)內(nèi)容提取正文
  • 將每一章放在一個(gè)文件或多章放在一個(gè)文件中
  • 網(wǎng)絡(luò)錯(cuò)誤,鏈接錯(cuò)誤读恃,文章內(nèi)容結(jié)束后斷開請(qǐng)求
const fs = require('fs')
const apis = require('./api')

/**
 * @param startId                       第一章id
 * @param fileName                      文件名
 * @param chapterSliceNumber            每隔多少章節(jié)分割一次文件
 * @param endNumber                     請(qǐng)求多少章節(jié)停止
 * @param continuousErrorCloseNumber    連續(xù)錯(cuò)誤關(guān)閉(次數(shù))
 */

class WriteChapter {
  #url = 'https://www.mht99.com/17023' // 網(wǎng)站
  #pageIndex = 0 // 頁(yè)碼
  #fileIndex = 1 // 文件下標(biāo)
  #finishChapterNumber = 0 // 已完成加載的數(shù)量
  #data = [] // 數(shù)據(jù)存放
  #continuousErrorNumber = 0 // 連續(xù)請(qǐng)求錯(cuò)誤次數(shù)
  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('錯(cuò)誤:傳入?yún)?shù)類型錯(cuò)誤隧膘!\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

這里將所有方法封裝到了一個(gè)類中西雀,如果是同一個(gè)站點(diǎn)互墓,可以直接使用。輸出內(nèi)容為 json 文件蒋搜,每一個(gè)標(biāo)點(diǎn)符號(hào)分割成一行篡撵,可以修改#write 方法,將輸出內(nèi)容改為自己需要的文件類型及各式豆挽。當(dāng)然育谬,最好是多設(shè)置幾個(gè)各式,可以根據(jù)參數(shù)選擇帮哈。

3膛檀、入口文件

const WriteChapter = require('./write')

const startId = 35254397
const fileName = '技能'

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

writeChapter.start()

引入 src/write 中的累,傳入?yún)?shù)娘侍,開始扒拉

六咖刃、扒拉完本

1、查看小說第一章的 id

2憾筏、查看小說最后一章的 id

3嚎杨、計(jì)算下整書大概有多少章節(jié),每章大概多少頁(yè)氧腰,可以計(jì)算出調(diào)用次數(shù)

4枫浙、防止中間斷開后需要繼續(xù),輸出的文件名可以設(shè)置為最后一次成功的 id

七古拴、講的好亂箩帚,其實(shí)沒啥東西,直接上鏈接好了

碼云倉(cāng)庫(kù)地址:https://gitee.com/webxingjie/get-novel/tree/master

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末黄痪,一起剝皮案震驚了整個(gè)濱河市紧帕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桅打,老刑警劉巖是嗜,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轻纪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡叠纷,警方通過查閱死者的電腦和手機(jī)刻帚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涩嚣,“玉大人崇众,你說我怎么就攤上這事『胶瘢” “怎么了顷歌?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)幔睬。 經(jīng)常有香客問我眯漩,道長(zhǎng),這世上最難降的妖魔是什么麻顶? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任赦抖,我火速辦了婚禮,結(jié)果婚禮上辅肾,老公的妹妹穿的比我還像新娘队萤。我一直安慰自己,他們只是感情好矫钓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布要尔。 她就那樣靜靜地躺著,像睡著了一般新娜。 火紅的嫁衣襯著肌膚如雪赵辕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天概龄,我揣著相機(jī)與錄音还惠,去河邊找鬼。 笑死旁钧,一個(gè)胖子當(dāng)著我的面吹牛吸重,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播歪今,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼颜矿!你這毒婦竟也來了寄猩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤骑疆,失蹤者是張志新(化名)和其女友劉穎田篇,沒想到半個(gè)月后替废,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泊柬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年椎镣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兽赁。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡状答,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刀崖,到底是詐尸還是另有隱情惊科,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布亮钦,位于F島的核電站馆截,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蜂莉。R本人自食惡果不足惜蜡娶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望映穗。 院中可真熱鬧翎蹈,春花似錦、人聲如沸男公。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)枢赔。三九已至澄阳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踏拜,已是汗流浹背碎赢。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留速梗,地道東北人肮塞。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像姻锁,于是被迫代替她去往敵國(guó)和親枕赵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容