优化 accessToken.js
该文件用于获取公众号凭证 access_token ,之前的代码如下:
var fs = require('fs')
var request = require('./request') // 将 request 封装成 Promise
module.exports = (config) => { // config 为配置文件,其中包含 appid 、appScrect ……
return new Promise((resolve, reject) => {
var currentTime = new Date().getTime() // 获取当前时间戳
var url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + config.appId + '&secret=' + config.appScrect // 发起请求
// 判断是否需要更新 accessToken
if (config.setAccessToken.accessToken === '' || config.setAccessToken.time < currentTime) {
// 若本地文件不存在凭证或凭证过期则更新本地重新凭证
// config.setAccessToken 为本地文件键名
request.get(url).then((response) => {
var result = JSON.parse(response)
config.setAccessToken.accessToken = result.access_token
config.setAccessToken.time = new Date().getTime() + (parseInt(result.expires_in) - 200) * 1000 // 计算过期时间
fs.writeFile('./config.json', JSON.stringify(config), (error) => {
if (error) {
reject(error)
}
resolve(response.access_token) // 导出 access_token
})
})
} else {
// 若凭证未过期直接导出本地 access_token
resolve(config.setAccessToken.accessToken)
}
})
}
以上实现方式是将请求结果通过 fs 模块直接写入本地文件存储,这样会导致代码略显臃肿(我暂时无法评估优化的利与弊,至少从代码量上看),而且在动态写入的过程中会导致配置文件文本格式被压缩不宜阅读。所以我将 access_token 的保存方式由本地保存修改未缓存储存,当然这里使用的是前文提到的 memory-cache 缓存组件。(之所以想到用 memory-cache 缓存模块是通过 基于nodejs 的微信 JS-SDK 简单应用 的启发)
memory-cache 用法回顾:
存储缓存。参数分别为:键名、键值、到期时间、回调函数
cache.put('houdini', 'disappear', 100, (key, value) => { console.log(key + ' did ' + value) })
读取缓存。参数为目标缓存的键名
cache.get('foo')
以下是我的具体“优化”操作:
// 首先将 fs 模块替换为 memory-cache
var cache = require('memory-cache')
// 之后通过 cache.get 去判断是否需要更新 access_token
if (!cache.get('accessToken')) { // 如果 access_token 不存在则将请求结果保存进缓存
request.get(url).then((response) => {
var result = JSON.parse(response)
cache.put('accessToken', result.access_token, (result.expires_in - 200) * 1000)
resolve(result.access_token) // 导出 access_token
})
} else { // 否则直接导出缓存中的 access_token
resolve(cache.get('accessToken'))
}
通过一番改造,代码量从 51 行生生压缩到了 22 行!个人甚是满意。
封装爬虫爬取页面数据
之前封装的 reply.js 是通过请求 wp-json 接口获得站点数据,从而实现诸如“自动被动回复”类的功能,这不免是在某些程度上降低了站点安全性。
cheerio 用法回顾:
const cheerio = require('cheerio') // 首先,引入模块 const $ = cheerio.load(html) // 获取整个 html 节点 const eleUl = $.find('#fruits') // 找到 id 为 fruits 的节点
// cheerio 模块对节点的使用方法 jQuery 如出一辙
在对 reply.js 进行优化前,我需要先封装一个专门用于爬取数据的页面,我将其命名为 reptile.js ,这里用到了前文的 cheerio 爬虫模块。
reptile.js 内包含两个爬取方法,分别用于爬取文章列表以及爬取正文。代码如下:
var cheerio = require('cheerio') // 引入模块
var config = require('./config') // 引入配置模块
var request = require('./request') // 将 request 封装成 Promise
exports.list = (callback, page) => { …… } // 回调函数用于向外抛出结果,page 为列表页 path
exports.detail = (url, callback) => { …… } // 回调函数用于向外抛出结果,url 为详情页 path
exports.list
因为站点文章列表和分类文章列表链接格式有区别,我做了区别处理,先看下我的路径:
首先我将公共部分 https://www.sfatpaper.com/ 提取到配置文件中,之后根据路由的配置对不同页面进行请求响应:
// router.js
function routerTo (res, page) {
reptile.list((data) => {
res.render('list.html', { // 渲染页面
data: data.list
})
}, page)
}
router.get('/history', (req, res) => { // 历史文章
routerTo(res, 'page/1')
})
router.get('/travel', (req, res) => { // 旅行日记
routerTo(res, 'travel/page/1')
})
router.get('/diary', (req, res) => { // 日常杂记
routerTo(res, 'diary/page/1')
})
router.get('/code', (req, res) => { // 码农笔记
routerTo(res, 'code/page/1')
})
// reptile.js - reptile.list
// config.reptilePath 即配置文件中的 https://www.sfatpaper.com/
request.get(config.reptilePath + page).then((response) => {
var html = ''
html += response // 将页面数据存入变量 html
var $ = cheerio.load(html)
function reptileData () {
var arr = [] // 用于存放文章数据
$('article').each((index, element) => { // 根据节点 $('article') 循环出所有文章
// 拆分路径的作用主要用于百度站长平台移动适配的链接匹配功能
var str = $(element).find('.entry-title a').attr('href').split('/')
arr.push({
sort: $(element).find('.meta-category a').text(), // 文章分类
title: $(element).find('.entry-title a').text(), // 文章标题
year: str[3],
month: str[4],
path: str[5],
excerpt: $(element).find('.entry-content p').text(), // 文章简介
thumbnail: $(element).find('.post-thumbnail img').attr('src') // 文章封面图
})
})
return arr // 返回爬取最终数组,此时 arr 为创建好的数组对象
}
callback(reptileData())
})
// list.html
// 这里需要重新将对应文章的链接拼接成正确的地址
{{each data}}
{{$value.title}}
{{$value.excerpt}}
{{/each}}
到此位置,列表页的渲染工作就到此为止。此时我们可以通过点击直接获取到对应路径的参数,从而定位爬取文章页面。
exports.detail
// router.js
router.get('/detail', (req, res) => {
var query = req.query // 获取到请求参数,从而拼接对应文章路径
// config.reptilePath 即配置文件中的 https://www.sfatpaper.com/
posts.detail(config.reptilePath + query.year + '/' + query.month + '/' + query.path, (detail) => {
res.render('detail.html', { // 后端处理数据
title: detail.title,
excerpt: detail.excerpt,
content: detail.content,
thumbnail: detail.thumbnail
})
})
})
// reptile.js - reptile.detail
request.get(config.reptilePath + url).then((response) => {
var html = ''
html += response // 将页面内容存入变量 html
var $ = cheerio.load(html)'
var postDetail = {
title: $('.entry-title').text(), // 文章标题
excerpt: $('*[name="description"]').attr('content'), // 文章简介
content: $('.entry-content').html(), // 文章正文
thumbnail: $('.entry-content img').eq(0).attr('src') // 文章正文首图
}
callback(postDetail)
})
这里有一点暂时没有想到好的办法,那就是文章缩略图和文章简介都显示与原站点列表页,通过层层传递的方式很繁琐,所以我将简介存到了页面 description 中,而图片则直接选择调取文章内容首图而非列表页的缩略图。
优化被动关键词自动回复
我的自定义回复文件命名为 reply.js ,具体详情还请移步至前文,这里主要优化的地方是最新文章回复的功能,这个功能可以通过回复字母“n”以及点击菜单“文章”->"最新文章"触发。
先来看下我之前的实现方法吧(以下代码只针对是获取最新文章功能):
// config.webJson 原 wp-json 接口地址
request.get(config.webJson).then((data) => {
var contentArr = []
var items = JSON.parse(data) // 将数据转化为对象数组
contentArr.push({
Title: item[0].title.rendered, // 文章标题
Description: item[0].excerpt.rendered.replace(/]+>/g, ''), // 文章简介(replace 解决数据中 html 标签显示问题)
PicUrl: item[0].thumbnail, // 文章缩略图
Url: item[0].link // 文章链接
})
// replyType.js 整合了所有回复的种类,包括图文回复、文本回复、图片回复 ……
resultXml = replyType.graphicMsg(result, contentArr) // 向 replyType.js 抛出最新文章数据
})
而现在我利用之前开发好的 reptile.js 模块,我们可以省去对 JSON.parse 数据转化工作(爬取来的内容为纯文本,带 html 标签),对于数据的调用也更为灵活,不会再拘泥于接口中繁杂的层级命名。
代码优化如下:
reptile.list((data) => {
var contentArr = [{
Title: data[0].title, // 文章标题
Description: data[0].excerpt, // 文章简介
PicUrl: data[0].thumbnail, // 文章缩略图
Url: config.routerPath + 'detail?year=' + data[0].year + '&month=' + data[0].month + '&path=' + data[0].path // 文章链接(拼接路径,用法如前文)
}]
resultXml = replyType.graphicMsg(result, contentArr) // 向 replyType.js 抛出最新文章数据
}, 'page/1') // 始终获取第一页的第一篇文章
怎么样,是不是看着多少顺眼了些呢?
前文还提到了另外一个模块 cron ,它是一个定时器用来定时去执行某样任务。起初是想利用该模块实现主动发送最新文章的功能,但由于种种原因该功能暂时被我放弃了。所以在这里也没有太多可以介绍的,若想要了解它还请异步至 cron 。
相关文章:
资料参考:
本文由博客一文多发平台 OpenWrite 发布!