Koa2 + Mongo + 爬虫 搭建 小说微信小程序(本地开发篇)

4次阅读

共计 4475 个字符,预计需要花费 12 分钟才能阅读完成。

前言:根据慕课网 Koa2 实现电影微信公众号前后端开发 学习后的改造
由于上下班期间会看会小说,但是无奈广告太多,还要收费,于是结合课程,进行开发,并上传到自己的微信小程序。

github
大致的思路:1. 连接数据库 2. 跑定时任务,进行数据库的更新 3. 开启接口服务 4. 微信小程序接口调用
1. 连接数据库
连接本地的 mongodb 数据库
const mongoose = require(‘mongoose’)
var db = ‘mongodb://localhost/story-bookShelf’

exports.connect = () => {
let maxConnectTimes = 0

return new Promise((resolve, reject) => {
if (process.env.NODE_ENV !== ‘production’) {
mongoose.set(‘debug’, false)
}

mongoose.connect(db)

mongoose.connection.on(‘disconnected’, () => {
maxConnectTimes++

if (maxConnectTimes < 5) {
mongoose.connect(db)
} else {
throw new Error(‘ 数据库挂了吧,快去修吧 ’)
}
})

mongoose.connection.on(‘error’, err => {
console.log(err)
maxConnectTimes++

if (maxConnectTimes < 5) {
mongoose.connect(db)
} else {
throw new Error(‘ 数据库挂了吧,快去修吧 ’)
}
})

mongoose.connection.once(‘open’, () => {
resolve()
console.log(‘MongoDB Connected successfully!’)
})
})
}
然后初始化定义好的 Schema
const mongoose = require(‘mongoose’)
const Schema = mongoose.Schema

const bookSchema = new Schema({
name: {
type: String
},
bookId: {
unique: true,
type: Number
}
})
……
mongoose.model(‘Book’, bookSchema)
2. 跑定时任务,进行数据库的更新
这一步骤主要是在定时进行数据库小说章节的更新,用的是 node-schedule 进行定时跑任务。

小说章节数是否增加,没增加不用进行爬取。同时在爬取的时候需要提前前 5 章爬取,避免一些作者为了占坑,提前写的预告。
每一本小说就开一个子进程 child_process 去跑,将数据存储到 mongo, 同时存储子进程对后续有用。
定时跑任务时候会遇到上一条任务还在跑,所以在每一次跑之前都清空一遍储存的子进程,将子进程杀掉。

章节任务
// chapter.js

const cp = require(‘child_process’)
const {resolve} = require(‘path’)
const mongoose = require(‘mongoose’)
const {childProcessStore} = require(‘../lib/child_process_store’) // 全局存储子进程

/**
*
* @param {书本 ID} bookId
* @param {从哪里开始查找} startNum
*/
exports.taskChapter = async(bookId, startNum = 0) => {

const Chapter = mongoose.model(‘Chapter’)

const script = resolve(__dirname, ‘../crawler/chapter.js’) // 真正执行爬虫任务模块
const child = cp.fork(script, []) // 开启 IPC 通道,传递数据
let invoked = false

// 这里等子进程将数据传回来,然后存储到 mongo 中(具体爬取看下一段代码)
child.on(‘message’, async data => {

// 先找一下是否有数据了
let chapterData = await Chapter.findOne({
chapterId: data.chapterId
})

// 需要将拿到的章节与存储的章节做对比 防止作者占坑
if (!chapterData) {
chapterData = new Chapter(data)
await chapterData.save()
return
}

// 进行字数对比 相差 50 字符
if ((data.content.length – 50 >= 0) && (data.content.length – 50 > chapterData.content.length)) {
Chapter.updateOne (
{chapterId: +data.chapterId},
{content : data.content}
);
}
})

child.send({// 发送给子进程进行爬取
bookId, // 哪本小说
startNum // 从哪个章节开始爬
})
// 存储所有章节的爬取 用于跑进程删除子进程
childProcessStore.set(‘chapter’, child)
}

真正开启爬虫,用的是 puppeteer,谷歌内核的爬取,功能很强大。分两步:1. 爬对应小说的章节目录,拿到章节数组 2. 根据传进来的 startNum 进行章节 startNum 的往后爬取
// crawler/chapter.js

const puppeteer = require(‘puppeteer’)
let url = `http://www.mytxt.cc/read/` // 目标网址

const sleep = time => new Promise(resolve => {
setTimeout(resolve, time)
})

process.on(‘message’, async book => {
url = `${url}${book.bookId}/`

console.log(‘Start visit the target page — chapter’, url)
// 找到对应的小说,拿到具体的章节数组
const browser = await puppeteer.launch({
args: [‘–no-sandbox’],
dumpio: false
}).catch(err => {
console.log(‘browser–error:’, err)
browser.close
})

const page = await browser.newPage()
await page.goto(url, {
waitUntil: ‘networkidle2’
})

await sleep(3000)

await page.waitForSelector(‘.story_list_m62topxs’) // 找到具体字段的 class

let result = await page.evaluate((book) => {
let list = document.querySelectorAll(‘.cp_dd_m62topxs li’)
let reg = new RegExp(`${book.bookId}\/(\\S*).html`)
let chapter = Array.from(list).map((item, index) => {
return {
title: item.innerText,
chapterId: item.innerHTML.match(reg)[1]
}
})
return chapter
}, book)

// 截取从哪里开始爬章节
let tempResult = result.slice(book.startNum, result.length)

for (let i = 0; i < tempResult.length; i++) {
let chapterId = tempResult[i].chapterId
console.log(‘ 开始爬 url:’, `${url}${chapterId}.html`)

await page.goto(`${url}${chapterId}.html`, {
waitUntil: ‘networkidle2’
})

await sleep(2000)

const content = await page.evaluate(() => {
return document.querySelectorAll(‘.detail_con_m62topxs p’)[0].innerText
})

tempResult[i].content = content
tempResult[i].bookId = book.bookId

process.send(tempResult[i]) // 通过 IPC 将数据传回去,触发 child.on(‘message’)
}
browser.close()
process.exit(0)
})
3. 开启接口
做的任务主要是,拿 mongodb 的数据,同时通过 koa-router 发布路由
先定义好路由装饰器,方便后续使用 具体看 decorator.js
底层拿到数据库的数据
service/book.js // 拿到数据库存储的值

const Chapter = mongoose.model(‘Chapter’)

// 获取具体的章节内容
export const getDetailChapter = async (data) => {
const chapter = await Chapter.findOne({
chapterId: data.chapterId,
bookId: data.bookId
}, {
content: 1,
title: 1,
chapterId: 1
})
// console.log(‘getDetailChapter::’, chapter)
return chapter
}

路由定义 后续的接口就是‘/api/book/chapter’
@controller(‘/api/book’)
export class bookController {
@post(‘/chapter’)
async getDetailChapter (ctx, next) {
const {chapterId, bookId} = ctx.request.body.data
const list = await getDetailChapter({
chapterId,
bookId
})

ctx.body = {
success: true,
data: list
}
}
}
4. 微信小程序
使用 wepy 进行开发,功能也是很简单,具体开发可以参见小程序代码,这里不做详细讲述。支持记录每一章的进度,与全局设置。后续可以自己发挥。在目标网站找到小说的 Id 之后就能进行查找了。接下来讲解部署到服务器细节。
最后,在这里特别感谢 @汪江 江哥的帮助,我前后琢磨了两个月,而他就用了三天,谢谢你不厌其烦的帮助,与你共事很开心。以上只是我的不成熟的技术,欢迎各位留言指教。

正文完
 0