Koa2 + Mongo + 爬蟲 搭建 小說微信小程序(本地開發篇)

前言:根據慕課網 Koa2 實現電影微信公衆號先後端開發 學習後的改造html

因爲上下班期間會看會小說,可是無奈廣告太多,還要收費,因而結合課程,進行開發,並上傳到本身的微信小程序。node

clipboard.png

githubgit

大體的思路:
1.鏈接數據庫
2.跑定時任務,進行數據庫的更新
3.開啓接口服務
4.微信小程序接口調用github

1.鏈接數據庫

鏈接本地的mongodb數據庫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進行定時跑任務。npm

  1. 小說章節數是否增長,沒增長不用進行爬取。同時在爬取的時候須要提早前5章爬取,避免一些做者爲了佔坑,提早寫的預告。
  2. 每一本小說就開一個子進程child_process去跑,將數據存儲到mongo, 同時存儲子進程對後續有用。
  3. 定時跑任務時候會遇到上一條任務還在跑,因此在每一次跑以前都清空一遍儲存的子進程,將子進程殺掉。

章節任務小程序

// 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以後就能進行查找了。
接下來說解部署到服務器細節。

最後,在這裏特別感謝@汪江 江哥的幫助,我先後琢磨了兩個月,而他就用了三天,謝謝你不厭其煩的幫助,與你共事很開心。以上只是個人不成熟的技術,歡迎各位留言指教。

相關文章
相關標籤/搜索