框架使用Express,开发前先下载Express到项目目录
npm i express //下载Express框架
npm i //下载框架所需的依赖
内网穿透,因为微信服务器需要访问到你的服务器域名,每次开机测试都要运行穿透软件
(113条消息) sunny-ngrok 的配置及使用_zxc_user的博客-CSDN博客
password:LKbswxl1314?
id:163732335504
还有运行之前一定要打开mongoose的服务也就是黑框:
(157条消息) mongodb 学习_qq_51717117的博客-CSDN博客
微信的参考网页
微信公众平台 (qq.com) 测试接口
关注/取消关注事件 | 微信开放文档 (qq.com) 测试文档
app.js主文件
//app.js
const express = require('express'); //引入express框架
const app = express(); //创建express实例app
app.use() //这里可以使用中间件,从而简化主文件
app.listen(6177, () => {
console.log('server is running...')
}) //开启server服务,6177是自定义端口
功能一:auth_check.js 验证服务器有效性
$对应微信开放文档=>开始开发/接入指南
单文件版(113条消息) 微信公众号_接口测试_验证服务器有效性_现实里的大梦想家的博客-CSDN博客
思路比较清晰,适合作为拆成模块的依据
模块化版
思路:
1.config定义配置对象写成模块,因为都是开发者固定的东西,且模块便于重复调用
//auth_check.js
module.exports = {
token: '1234', //身份验证的参数,自定义即可
appID: 'wx387707c142342e5f', //根据自己的开发者信息填写
appsecret: 'ed1109d567b0bf87eba63747f6aa71b2' //根据自己的开发者信息填写
}
2.在auth_check.js文件中调用模块
验证思路:
微信服务器发来的东西即=>req.query
{
signature: '9bf986bba167e1d38434813148d9622cf1e143c0',
//微信的加密签名,在自己服务器根据这个算出是否来自微信服务器
echostr: '3329679419479420731', //微信随机字符串
timestamp: '1635763993', //微信发送请求的时间戳
nonce: '378478086' //随机数字
}
解密思路:
将微信服务器发来的timestamp, nonce, token字典排序拼接成字符串sha1加密(所以要引入sha1), 如果和req.query中signature一样, 验证成功,返回给微信服务器
//auth_check.js
const sha1 = require('sha1') //引入解密模块
const config = require('./config') //引入配置模块config,注意路径
module.exports = () => { //暴露模块
return async(req, res, next) => {
//微信服务器向开发者服务器请求的参数是req.query
const { signature, echostr, timestamp, nonce } = req.query
//对象的结构赋值,拿到大括号中想提取的内容
const { token } = config//拿到自定义的token进行对来自微信服务器的signature解密
const sha1str = sha1([timestamp, nonce, token].sort().join(''))//按思路解密
if (req.method === 'GET') {
if (sha1str == signature) { //对应得上,身份验证成功
res.send(echostr) //微信测试号收到后即知道服务器身份验证通过
} else { //没对应上,不是微信服务器
res.end('err')
}
} else if (req.method === 'POST') { //post后面再说
} else { //既不是get也不是post,微信识别不了,报错
res.end('err')
}
}
}
/*微信服务器会发送两种请求到开发者服务器
GET
-验证服务器的有效性
POST
-微信会把用户输入的消息转发到开发者服务器上
*/
注意:在app.js中要引入这个身份验证中间件
const express = require('express');
const auth = require('./auth_check') //身份验证主模块,里面包括config
const app = express();
app.use(auth()) //使用身份验证中间件
app.listen(6177, () => {
console.log('server is running...')
})
功能二:获取Access_Token
$对应微信开发者文档=>开始开发/获取AccessToken
为什么需要Access_Token? 因为是微信调用全局接口的唯一凭据且有效期两小时
思路:
首次本地没有,向微信服务器请求access_token,保存(本地文件),第二次先去本地读取文件,判断是否过期,过期了:重新请求,覆盖之前文件;没过期:直接使用
流程:(调用模块化方法)
读取本地文件:readAccessToken
本地文件存在:
判断是否过期 isValidAccessToken
getAccessToken,saveAccessToken
本地文件不存在:
请求 getAccessToken,saveAccessToken
//accesstoken.js
const { appID, appsecret } = require('../config') //引入一些配置信息
const { writeFile, readFile } = require('fs') //使用node自带的文件模块,获取读写文件方法
const rp = require('request-promise-native') //npm i request库,request-promise-native库
class wechat {
getAccessToken() {
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}` //请求地址
return new Promise((resolve, reject) => {
rp({ method: 'GET', url, json: true })
.then(res => {
res.expires_in = Date.now() + (res.expires_in - 300) * 1000;
resolve(res)
})
.catch(err => {
reject('getAccessToken方法出问题' + err)
})
})
}
saveAccessToken(accessToken) {
accessToken = JSON.stringify(accessToken) //将对象转化成json字符串
return new Promise((resolve, reject) => {
writeFile('./accessToken.txt', accessToken, (err) => {
if (!err) {
console.log('save success')
resolve()
} else {
reject('error:' + err)
}
})
})
}
readAccessToken() {
return new Promise((resolve, reject) => {
readFile('./accessToken.txt', (err, data) => {
if (!err) {
console.log('read success')
data = JSON.parse(data) //json字符串转换为json对象
resolve(data)
} else {
reject('error:' + err)
}
})
})
}
isValidToken(data) {
if (!data && !data.access_token && !data.expires_in) { return false }
//检测有效期
if (data.expires_in < Date.now()) { return false } else { return true }
}
fetchAccessToken() {
if (this.access_token && this.expires_in && this.isValidToken(this)) {
//有有效的token
return Promise.resolve({
access_token: this.access_token,
expires_in: this.expires_in
})
}
return this.readAccessToken()
.then(async res => { //判断是否过期
if (this.isValidToken(res)) { return Promise.resolve(res) } else {
const res = await this.getAccessToken()
await this.saveAccessToken(res) //保存token
return Promise.resolve(res)
}
})
.catch(async err => { //本地没有文件
const res = await this.getAccessToken()
await this.saveAccessToken(res) //保存token
return Promise.resolve(res)
})
.then(res => { //挂载到this上
this.access_token = res.access_token
this.expires_in = res.expires_in
return Promise.resolve(res) //最终的返回值
})
}
}
//测试用
const w = new wechat();
w.fetchAccessToken().then(res => { console.log(res) }) //这样就可以拿数据
功能三:获取用户发送的消息
$对应微信开发者文档=>基础消息能力 因为是post请求,所以在前面空着的post框架里写
微信服务器会发送两种类型的消息给开发者服务器
1.GET请求
-验证服务器的有效性
2.POST请求
-微信服务器会将用户发送的数据以POST请求的方式转发到开发者服务器上
//auth.js
//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
module.exports = () => {
return async(req, res, next) => {
const { signature, echostr, timestamp, nonce } = req.query
const { token } = config
const sha1str = sha1([timestamp, nonce, token].sort().join(''))
if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
if (sha1str == signature) {
res.send(echostr)
} else {
res.end('error')
}
} else if (req.method === 'POST') { //验证消息来自于微信服务器
if (sha1str !== signature) { //消息不是来自微信服务器
res.end('error')
} //----------------------------------
const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
const jsData = await praseXMLAsync(xmlData)
const message = formatMessage(jsData) //格式化数据
console.log(message) //接收到了用户发来的消息
res.end('')
//----------------------------------
} else { //其他类型的请求都是无效的
res.end('error')
}
}
}
//tools.js
const { parseString } = require('xml2js') //npm i xml2js
module.exports = {
getUserDataAsync(req) {
return new Promise((resolve, reject) => {
let xmlData = ''
req
.on('data', data => { //将流式数据注入到回调函数中
xmlData += data.toString() //将buffer转换为字符串
})
.on('end', () => {
resolve(xmlData)
})
})
},
praseXMLAsync(xmlData) {
return new Promise((resolve, reject) => {
parseString(xmlData, { trim: true }, (err, data) => {
if (!err) {
resolve(data)
} else {
reject('praseXMLAsync' + err)
}
})
})
},
formatMessage(jsData) {
let message = {}
jsData = jsData.xml //获取xml对象
if (typeof jsData === 'object') { //判断是否是一个对象
for (let key in jsData) {
let value = jsData[key] //获取属性值
if (Array.isArray(value) && value.length > 0) { //过滤掉空数据
message[key] = value[0]
}
}
}
return message
}
}
收到消息:
功能四:被动回复用户消息
1.简易版
//auth.js被动回复文本消息
//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
module.exports = () => {
return async(req, res, next) => {
const { signature, echostr, timestamp, nonce } = req.query
const { token } = config
const sha1str = sha1([timestamp, nonce, token].sort().join(''))
if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
if (sha1str == signature) {
res.send(echostr)
} else {
res.end('error')
}
} else if (req.method === 'POST') { //验证消息来自于微信服务器
if (sha1str !== signature) { //消息不是来自微信服务器
res.end('error')
} //----------------------------------
const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
const jsData = await praseXMLAsync(xmlData)
const message = formatMessage(jsData) //格式化数据
console.log(message) //接收到了用户发来的消息
let content = '我听不懂'
if (message.MsgType === 'text') { //判断类型
if (message.Content === '1') {
content = '嘿嘿'
}
}
let replyMessage = `<xml>
<ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${content}]]></Content>
</xml>`
res.send(replyMessage)
//----------------------------------
} else { //其他类型的请求都是无效的
res.end('error')
}
}
}
功能单一,代码冗杂,改成模板工具函数进行调用
2.多文件版 ,可回复文本,语音,图片消息
//验证开发者服务器有效性
const config = require('../config')
const sha1 = require('sha1')
const { getUserDataAsync, praseXMLAsync, formatMessage } = require('../utils/tools')
const template = require('./template')
const reply = require('./reply')
module.exports = () => {
return async(req, res, next) => {
const { signature, echostr, timestamp, nonce } = req.query
const { token } = config
const sha1str = sha1([timestamp, nonce, token].sort().join(''))
if (req.method === 'GET') { //微信服务器验证开发者服务器有效性时用GET
if (sha1str == signature) {
res.send(echostr)
} else {
res.end('error')
}
} else if (req.method === 'POST') { //验证消息来自于微信服务器
if (sha1str !== signature) { //消息不是来自微信服务器
res.end('error')
} //----------------------------------
const xmlData = await getUserDataAsync(req) //接受请求体中的流式数据
const jsData = await praseXMLAsync(xmlData)
const message = formatMessage(jsData) //格式化数据
console.log(message) //接收到了用户发来的消息
const options = reply(message)
let replyMessage = template(options)
res.send(replyMessage)
//----------------------------------
} else { //其他类型的请求都是无效的
res.end('error')
}
}
}
template.js模板=>微信开发者文档有规定xml格式
//消息回复模板函数
module.exports = options => {
let replymessage = `<xml>
<ToUserName><![CDATA[${options.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${options.ToUserName}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[${options.MsgType}]]></MsgType>`
if (options.MsgType === 'text') {
replymessage += `<Content><![CDATA[${options.Content}]]></Content>`
} else if (options.MsgType === 'image') {
replymessage += `<Image><MediaId><![CDATA[${options.MediaId}]]></MediaId></Image>`
} else if (options.MsgType === 'voice') {
replymessage += `<Voice><MediaId><![CDATA[${options.MediaId}]]></MediaId></Voice>`
}
replymessage += '</xml>'
return replymessage
}
后面还加了图文消息的模板,因为少了一个<item>导致一直收不到消息
错误修正后,发现只能收到一条图文消息,查看官方文档:
图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
所以说用户发文本消息只能回复一条,而自定义菜单关联的事件可以恢复多条!
点击“赞我们” 即可收到三条图文消息(当然这个对应的不是很好,不过我已经很满意了)
reply.js
module.exports = (message) => {
let options = {
ToUserName: message.ToUserName,
FromUserName: message.FromUserName,
CreateTime: Date.now(),
MsgType: 'text',
}
let content = '我不明白 ~'
if (message.MsgType === 'text') {
if (message.Content === '5') {
content = '👉'
}
} else if (message.MsgType === 'image') {
options.MsgType = 'image'
options.MediaId = message.MediaId
} else if (message.MsgType === 'voice') {
options.MsgType = 'voice'
options.MediaId = message.MediaId
console.log(message.Recognition)
}
options.Content = content
return options
}
至此微信公众号可以完成与用户的普通消息收发
事件消息和普通消息差不多 (/≧▽≦)/
else if (message.MsgType === 'event') {
if (message.Event === 'subscribe') {
content = 'Welcome!'
} else if (message.Event === 'unsubscribe') {
console.log('lose a fan...')
}
}
微信小程序也开放了推送功能
templateMessage.send | 微信开放文档 (qq.com)
功能五 自定义菜单
//accesstoken.js
createmenu(menu) {
return new Promise(async(resolve, reject) => {
const data = await this.fetchAccessToken()
const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${data.access_token}`
const result = await rp({ method: 'POST', url, json: true, body: menu })
resolve(result)
})
}
deletemenu() {
return new Promise(async(resolve, reject) => {
const data = await this.fetchAccessToken()
const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${data.access_token}`
const result = await rp({ method: 'GET', url, json: true })
resolve(result)
})
}
//立即执行函数测试一下
(async() => {
const w = new wechat()
let result = await w.deletemenu()
console.log(result)
let date = await w.createmenu(menu)
console.log(date)
const data = await w.fetchTicket()
console.log(data)
})()
//menu.js别忘了在accesstoken里引入
module.exports = {
"button": [{
"type": "view",
"name": "影片",
"url": "http://publicvv.free.idcfengye.com/movie"
},
{
"name": "菜单",
"sub_button": [{
"type": "view",
"name": "热门数据",
"url": "http://publicvv.free.idcfengye.com/search"
},
{
"type": "click",
"name": "赞我们",
"key": "V1001_GOOD"
}
]
}
]
}
//在事件里加上对应的reply.js
else if (message.MsgType === 'event') {
if (message.Event === 'subscribe') {
content = 'Welcome!'
} else if (message.Event === 'unsubscribe') {
console.log('lose a fan...')
} else if (message.Event === 'CLICK') {
if (message.EventKey === 'V1001_TODAY_MUSIC') {
content = '《新年好》⭐'
} else if (message.EventKey === 'V1001_GOOD') {
content = '谢谢支持😊'
}
}
功能六 JS-SDK接口获取
一、获取ticket(和accesstoken差不多)
//accesstoken.js
getTicket() {
return new Promise(async(resolve, reject) => {
const data = await this.fetchAccessToken()
console.log(data)
const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${data.access_token}&type=wx_card`
rp({ method: 'GET', url, json: true })
.then(res => {
resolve(res)
})
.catch(err => {
reject('getticket方法出问题' + err)
})
})
}
saveTicket(ticket) {
ticket = JSON.stringify(ticket) //将对象转化成json字符串
return new Promise((resolve, reject) => {
writeFile('./ticket.txt', ticket, (err) => {
if (!err) {
console.log('save success')
resolve()
} else {
reject('error:' + err)
}
})
})
}
readTicket() {
return new Promise((resolve, reject) => {
readFile('./ticket.txt', (err, data) => {
if (!err) {
console.log('read success')
data = JSON.parse(data) //json字符串转换为json对象
resolve(data)
} else {
reject('error:' + err)
}
})
})
}
isValidTicket(data) {
if (!data && !data.ticket && !data.expires_in) { return false }
//检测有效期
if (data.expires_in < Date.now()) { return false } else { return true }
}
fetchTicket() {
if (this.ticket && this.ticket_expires_in && this.isValidTicket(this)) {
//有有效的token
return Promise.resolve({
ticket: this.ticket,
expires_in: this.expires_in
})
}
return this.readTicket()
.then(async res => { //判断是否过期
if (this.isValidTicket(res)) { return Promise.resolve(res) } else {
const res = await this.getTicket()
await this.saveTicket(res) //保存token
return Promise.resolve(res)
}
})
.catch(async err => { //本地没有文件
const res = await this.getTicket()
await this.saveTicket(res) //保存token
return Promise.resolve(res)
})
.then(res => { //挂载到this上
this.access_token = res.access_token
this.ticket_expires_in = res.expires_in
return Promise.resolve(res) //最终的返回值
})
}
生成js-sdk使用的签名:
1.组合参与签名的四个参数:jsapi_ticket(临时票据)、noncestr(随机字符串)、timestamp(时间戳)、url(当前服务器地址)
2.将其进行字典排序,以‘&’拼接在一起
3.进行sha1加密,最终生成signature
//npm i ejs 引入模板引擎
app.set('views', './views')
app.set('view engine', 'ejs')
//配置模板引擎
app.get('/search', async(req, res) => {
//通过config接口注入权限验证配置
const noncestr = num.randomStr(36)
const timestamp = Date.now()
const { ticket } = await AccessTicket.fetchTicket()
const arr = [`jsapi_ticket=${ticket}`, `noncestr=${noncestr}`, `timestamp=${timestamp}`, `url=${url}/search`]
const str = arr.sort().join('&')
console.log(str)
const signature = sha1(str)
res.render('search', {
signature,
noncestr,
timestamp
})
})
//随机字符串代码
module.exports = {
randomNum(min = 0, max = 100, len = 0) {
return Number((min + (max - min) * Math.random()).toFixed(len));
},
randomStr(len = 8) {
let str = '';
let list = '0123456789abcdefghijklmnopqrstuvwxyz';
for (let i = 0; i < len; i++) {
let index = this.randomNum(0, 35);
let word = list[index];
if (isNaN(word) && this.randomNum() < 50) {
word = word.toUpperCase();
}
str += word;
}
return str;
}
}
//ejs模板html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1 id='search'>这是一个搜索页面</h1>
<script type="text/javascript" src='http://res.wx.qq.com/open/js/jweixin-1.6.0.js '></script>
<script type="text/javascript">
wx.config({
debug: true,
appId: 'wx387707c142342e5f',
timestamp: '<%=timestamp%>',
nonceStr: '<%=noncestr%>',
signature: '<%=signature%>',
jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice']
});
wx.ready(function() {
//验证接口是否有权限
wx.checkJsApi({
jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice'],
success: function(res) {
console.log(res)
}
});
//语音识别
});
wx.error(function(res) {
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});
</script>
</body>
</html>
新下载了一个库BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务 zepto
复制 <script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.0rc1/zepto.min.js"></script>
然后用到了jQuery的东西,实现点击文字,返回热门电影信息功能
豆瓣api暂时无法使用,用了另外一个网址qpi
iiiiiii1/douban-imdb-api: 一个基于豆瓣、IMDB、烂番茄评分的电影电视剧双语(中英)数据api接口 (github.com)
获取的信息太多,只选了data[0] 肖申克的救赎,以下是网址返回内容第一条:
{
"data":
[{"createdAt":1605355459692,
"updatedAt":1605355459692,
"id":"5f968bfcee3680299115bbe6",
"poster":"https://wmdb.querydata.org/movie/poster/1603701754760-c50d8a.jpg",
"name":"肖申克的救赎",
"genre":"犯罪/剧情",
"description":"20世纪40年代末,小有成就的青年银行家安迪(蒂姆·罗宾斯 Tim Robbins 饰)因涉嫌杀害妻子及她的情人而锒铛入狱。在这座名为鲨堡的监狱内,希望似乎虚无缥缈,终身监禁的惩罚无疑注定了安迪接下来...",
"language":"英语",
"country":"美国",
"lang":"Cn",
"shareImage":"https://wmdb.querydata.org/movie/poster/1605355459683-5f968bfaee3680299115bb97.png",
"movie":"5f968bfaee3680299115bb97"
}],
"createdAt":1603701756481,
"updatedAt":1603701756481,
"id":"5f968bfaee3680299115bb97",
"originalName":"The Shawshank Redemption",
"imdbVotes":2297852,
"imdbRating":"9.3",
"rottenRating":"91",
"rottenVotes":75,
"year":"1994",
"imdbId":"tt0111161",
"alias":"月黑高飞(港) / 刺激1995(台) / 地狱诺言 / 铁窗岁月 / 消香克的救赎",
"doubanId":"1292052",
"type":"Movie",
"doubanRating":"9.7",
"doubanVotes":2170679,
"duration":8520,
"dateReleased":"1994-09-10"
}
$('#search').tap(function() { //点击就搜索电影
var url = 'https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn'
$.getJSON(url,
function(data) {
alert(data[0].data[0].name)
})
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width= device-width,intial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1 id='search'>这是一个搜索页面</h1>
<ul id="list">
</ul>
<script type="text/javascript " src='http://res.wx.qq.com/open/js/jweixin-1.6.0.js '></script>
<script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.0rc1/zepto.min.js "></script>
<script type="text/javascript ">
wx.config({
debug: true,
appId: 'wx387707c142342e5f',
timestamp: '<%=timestamp%>',
nonceStr: '<%=noncestr%>',
signature: '<%=signature%>',
jsApiList: ['updateAppMessageShareData', 'startRecord', 'stopRecord', 'translateVoice']
});
wx.ready(function() {
//验证这些接口在用户那里是否有权限
wx.checkJsApi({
jsApiList: ['updateAppMessageShareData'],
success: function(res) {
console.log(res)
}
});
//设置标志位,都不大能用了
$('#search').tap(function() { //点击就搜索电影
var url = 'https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn'
$.getJSON(url,
function(data) {
var html = ''
data.forEach(function(item) {
var da = item.data[0]
html += '<h2>' + da.name +
'</h2>' +
'<p>genre: ' + da.genre + '</p>' +
'<div>' +
'<img src ="' + da.poster + '" alt="图片">' +
'</div>'
$('#list').html(html)
})
})
})
});
wx.error(function(res) {
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});
</script>
</body>
</html>
易点微信编辑器_微信公众号排版_公众号软文编辑_公众号图文编辑_微信图文排版工具_微信内容排版软件_微信公众平台素材编辑-易点编辑器 (wxeditor.com)
LKbswxl1314?
功能七 爬取热门电影信息
官网:puppeteer/puppeteer: Headless Chrome Node.js API (github.com)
npm i puppeteer --save-dev
下载爬虫包,爬虫就是用代码的方式模拟人的手动点击获取信息
去到想去的网页,右击检查查看代码
const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/cinema/nowplaying/nanjing/'
module.exports = async() => {
//1.打开浏览器
const browser = await puppeteer.launch({
//args: [],
headless: false
})
//2.创建一个标签页
const page = await browser.newPage()
//3.跳转到指定网页
await page.goto(url, {
waitUntil: 'networkidle2' //等待网络空闲时跳转加载页面
})
//4.等待网址加载完成,爬取数据
const result = await page.evaluate(() => {
//对加载好的页面进行dom操作
//获取所有热门电影的li
let result = []
const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
for (let i = 0; i < 3; i++) {
const liDom = $list[i]
let title = $(liDom).data('title')
let score = $(liDom).data('score')
let region = $(liDom).data('region')
let href = $(liDom).find('.poster>a').attr('href')
let image = $(liDom).find('.poster>a>img').attr('src')
result.push({
title,
score,
region,
href,
image
})
}
return result
})
console.log(result)
//5.将浏览器关闭
await browser.close()
}
把影片类型和影片简介也加上
在href的网站里重新访问,将类型和简介挂载到结果上
const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/cinema/nowplaying/nanjing/'
module.exports = async() => {
//1.打开浏览器
const browser = await puppeteer.launch({
//args: [],
headless: false
})
//2.创建一个标签页
const page = await browser.newPage()
//3.跳转到指定网页
await page.goto(url, {
waitUntil: 'networkidle2' //等待网络空闲时跳转加载页面
})
//4.等待网址加载完成,爬取数据
const result = await page.evaluate(() => {
//对加载好的页面进行dom操作
//获取所有热门电影的li
let result = []
const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
for (let i = 0; i < 3; i++) {
const liDom = $list[i]
let title = $(liDom).data('title')
let score = $(liDom).data('score')
let region = $(liDom).data('region')
let href = $(liDom).find('.poster>a').attr('href')
let image = $(liDom).find('.poster>a>img').attr('src')
result.push({
title,
score,
region,
href,
image
})
}
return result
})
console.log(result)
for (var i = 0; i < result.length; i++) {
let item = result[i]
let url = item.href //获取电影详情页面的网址
await page.goto(url, {
waitUntil: 'networkidle2'
})
let itemresult = await page.evaluate(() => {
let genre = []
const $genre = $('[property="v:genre"]')
for (let j = 0; j < $genre.length; j++) {
genre.push($genre[j].innerText)
}
const summary = $('[property="v:summary"]').html().replace(/\s+/g, '')
return { genre, summary }
})
item.genre = itemresult.genre //挂载新属性
item.summary = itemresult.summary
}
console.log(result)
//5.将浏览器关闭
await browser.close()
}
新建数据库进行数据持久化保存
PS C:\Users\Vxl\Desktop\work\Gongzhongh> npm i mongoose
新的数据库相关目录结构:
model文件夹中定义存储的数据表类型
db连接mongoose数据库
save保存爬取的数据到数据库中
index整合以上功能完成数据持久化
立即执行函数有个奇怪的报错:(157条消息) TypeError: require(...)(...) is not a function_d1063270962的博客-CSDN博客
mongoose官方文档:
写代码的时候要注意好逻辑关系,如果爬取数据返回的是一个数组,在保存的时候就要对这个数组进行遍历依次拿到每一个结构体再进行数据存储。
db/index.js
const mongoose = require('mongoose')
module.exports = new Promise((resolve, reject) => {
mongoose.connect('mongodb://localhost:27017/gongzhonghao', { useNewUrlParser: true })
mongoose.connection.once('open', err => {
if (!err) {
console.log('数据库连接成功')
resolve()
}
})
})
model/theater.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema;
//创建约束对象
const theaterSchema = new Schema({
title: String,
score: Number,
region: String,
image: String,
genre: [String],
summary: String,
postKey: String, //图片要上传到七牛中返回的key值
createTime: {
type: Date,
defult: Date.now()
}
})
//创建模型对象
const Theater = mongoose.model('Theater', theaterSchema)
//暴露
module.exports = Theater
server/save/save.js
const Theater = require('../../model/Theater')
module.exports = async(data) => {
const result = await Theater.create({
title: data.title,
score: data.score,
region: data.region,
image: data.image,
genre: data.genre,
summary: data.summary,
})
console.log('数据保存成功' + result)
}
server/index.js
const theatercrawler = require('./crawler/theaters');
const db = require('../db');
const saveTheater = require('./save/save');
(async() => {
await db; //连接数据库
const data = await theatercrawler() //爬取数据
//返回一个有三个结构体元素的数组
for (var i = 0; i < 3; i++) { //就设定为三条
await saveTheater(data[i])
}
})()
通过图文消息将电影数据回复给用户
1.从数据库中查询数据
2.在处理用户消息回复区域返回数据(在文件中引入Theater)
(157条消息) 使用mongoose模块中的find方法查不到mongoDB数据_牛先森家的博客-CSDN博客
(157条消息) mongoose踩坑: Cannot overwrite xxxx model once compiled._jolieLi2019888的博客-CSDN博客
很详细的mongoose博客:(157条消息) 后端-Node(express)连接mongodb到前端-访问接口将数据显示页面(一条龙)_简单Cere-CSDN博客_express 连接mongodb 目前还没有完成数据从数据库中的读取
无语了...解决了,原来是没有建立数据库连接...😊
实现电影详情页面
用户发送 “热门” 返回三条电影图文消息,其中有个参数是电影详情网址
//item.doubanid由数据库提取
url: `http://publicvv.free.idcfengye.com/detail/${item.doubanId}`
我们用id这种唯一标识来对应每个电影的详情页面
接下来撰写这个详情页面
在router中加入电影详情的路由
router.get('/detail/:id', async(req, res) => {
const { id } = req.params //拿到网址中的doubanid
if (id) { //找到这个id的所有数据,引入Theaters
const data = await Theater.findOne({ doubanId: id }, { createTime: 0 }) //可以设置不要哪些数据
res.render('detail', { data }) //页面渲染过去,将后台的数据传给前端
} else {
res.end('error')
}
})
前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<header>
<a class="header_title" href="/movie">硅谷电影</a>
<a class="header_search" href="/search">搜索</a>
<!-- a,p标签和h标签有什么区别啊 -->
<div class="page">
<h1 class="title">
<%= data.title %>
</h1>
<section class="info">
<div class="left">
<p class="rating">
<span>评分:</span>
<strong><%= data.score %></strong>
<span class="ratingNum">52996人评价</span>
</p>
<p class="meta">
<%= data.runtime%> /
<% data.genre.forEach(function (item) { %>
<%= item %> /
<% })%>
</p>
</div>
<div class="right">
<a href="javascript:">
<img src="<%= data.image %>" alt="<%= data.title %>">
</a>
</div>
</section>
<section class="intro">
<h2>
<%= data.title %> 剧情简介
<div class="bd">
<p>
<%= data.summary %>
</p>
</div>
</h2>
</section>
</div>
</header>
</body>
</html>
数据库可能还需要改一下,没有设置id等等
(159条消息) jQuery获取指定ul下的li_bigface_girl的博客-CSDN博客_jquery获取ul下所有li
终于获取到了,就是多了一个$的问题 😀
const $list = $('#nowplaying>.mod-bd>.lists>.list-item')
const liDom = $list[i]
let doubanid = liDom.id
功能九 预告片实现
去有预告片素材的网页爬取预告片数据
又是一个低级的错误:TypeError: Assignment to constant variable.
把变量赋给了常量,因为没有写let关键词...
除了没获取到视频背景图片其他都拿到了
const puppeteer = require('puppeteer')
const url = 'https://movie.douban.com/coming'
module.exports = async() => {
const browser = await puppeteer.launch({
headless: false
})
const page = await browser.newPage()
await page.goto(url, { //去即将上映网页即有预告片的网页上进行爬取
waitUntil: 'networkidle2'
})
const result = await page.evaluate(() => {
let result = []
const $trs = $('.coming_list>tbody>tr')
for (let i = 0; i < $trs.length; i++) {
const trDom = $trs[i]
let num = parseInt($(trDom).find('td').last().html())
if (num > 20000) {
let href = $(trDom).find('a').attr('href')
result.push(href)
}
}
return result //result里放着各个电影的详情网址
})
let arr = []
for (var i = 0; i < result.length; i++) {
let hrefurl = result[i]
await page.goto(hrefurl, { //在电影详情页里爬更多数据
waitUntil: 'networkidle2'
})
let itemresult = await page.evaluate(() => {
let title = $('[property="v:itemreviewed"]').html()
let director = $('[rel="v:directedBy"]').html()
let casts = []
for (var j = 0; j < 3; j++) {
casts.push($('[rel="v:starring"]')[j].innerText)
}
let genre = []
const $genre = $('[property="v:genre"]')
for (let j = 0; j < $genre.length; j++) {
genre.push($genre[j].innerText)
}
const summary = $('[property="v:summary"]').html().replace(/\s+/g, '')
const runtime = $('[property="v:runtime"]').html()
const releaseDate = $('[property="v:initialReleaseDate"]')[0].innerText
const href = $('.related-pic-video').attr('href') //爬取预告片网址
const cover = $('.related-pic-video').attr('background-image') //这个好像没拿到
return {
title,
director,
casts,
genre,
summary,
runtime,
releaseDate,
href,
cover
}
})
arr.push(itemresult)
}
//预告电影链接
for (let i = 0; i < arr.length; i++) {
let item = arr[i]
let url = item.href
await page.goto(url, {
waitUntil: 'networkidle2'
})
item.link = await page.evaluate(() => { //将预告片地址挂载到每个{}里
const link = $('video>source').attr('src')
return link
})
}
await browser.close()
console.log(arr)
//5.将浏览器关闭
return arr
}
发送模板消息
sendmodelmes() { //发送模板消息
return new Promise(async(resolve, reject) => {
const ACCESS_TOKEN = await this.fetchAccessToken()
url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${ACCESS_TOKEN}`
const result = await rp({ method: 'POST', url, json: true, body: modelstruct })
resolve(result)
})
}
获取公众号用户列表
getuserlist() {
return new Promise(async(resolve, reject) => {
const ACCESS_TOKEN = await this.fetchAccessToken()
url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${ACCESS_TOKEN}`
rp({ method: 'GET', url, json: true })
.then(res => {
resolve(res)
})
.catch(err => {
reject('getuserlist方法出问题' + err)
})
})
}
公众号页面素材
Scale
https://2.flexiple.com/scale/home
Vektors
https://www.vektors.pro/
Pixeltrue
https://www.pixeltrue.com/free-illustrations
getillustrations
https://www.getillustrations.com/illustration-packs
oblikstudio免费插画包
https://gumroad.com/oblikstudioWeareSkribbl
https://weareskribbl.com/
Aracreator
https://www.aracreator.com/
Niceillustrations
https://niceillustrations.com/free-illustrations/
Open Peeps
https://www.openpeeps.com/
VectorCreator
https://icons8.com/vector-creator
Fresh Folk
https://fresh-folk.com/
Iconscout
https://iconscout.com/free-illustrations
Draw Kit
https://www.drawkit.io/
Humaaans
https://www.humaaans.com
Gallery.manypixels
https://gallery.manypixels.co/
Mixkit Art
https://mixkit.co/art/
Isoflat
https://isoflat.com/
IRA Design
https://iradesign.io/
Undraw
https://undraw.co/illustrations
Lukaszadam
https://lukaszadam.com/illustrations
设计网页