申明:上班摸魚不好,扒拉小說也不對(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