微信公众号接口开发Api,API连接篇

大雄 341 2022-09-06

微信公众号API开发文档

https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Network_Detection.html

 

实例(这里只已获取access_token为例):

#! /usr/bin/env python
# -*- coding: utf-8 -*-


"""
time   : 2020-07-16
notive : add WeChar IP white list
"""

import requests
import json


class WeChat(object):
    """
    -1  系统繁忙,此时请开发者稍候再试
    0   请求成功
    40001   AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性
    40002   请确保grant_type字段值为client_credential
    40164   调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。(小程序及小游戏调用不要求IP地址在白名单内。)
    89503   此IP调用需要管理员确认,请联系管理员
    89501   此IP正在等待管理员确认,请联系管理员
    89506   24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用
    89507   1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用
    """
    def __init__(self):
        self._access_token = ""
        self._base_url = "https://api.weixin.qq.com"
        self._bak_url = "https://api2.weixin.qq.com"

    def _get_access_token(self):
        try:
            print("正在获取 WeChat access")
            app_id = "wx***********"
            app_secret = "*********************"
            get_url = "{0}/cgi-bin/token?grant_type=client_credential&appid={1}&secret={2}".format(self._base_url, app_id, app_secret)
            res = requests.get(url=get_url)

            if res.status_code != 200 or res.text is None:
                get_url = "{0}/cgi-bin/token?grant_type=client_credential&appid={1}&secret={2}".format(self._bak_url, app_id, app_secret)
                res = requests.get(url=get_url)
                if res.status_code != 200 or res.text is None:
                    print("获取 WeChar url error")
                    return False

            res_wechar = json.loads(res.text)
            if "errcode" in res_wechar:
                print("WeChar _get_access_token error {0}:{1}".format(res_wechar["errcode"], res_wechar["errmsg"]))
                return False

            #print("access_token:", res_wechar["access_token"])
            #print("expires_in:", res_wechar["expires_in"])
            self._access_token = res_wechar["access_token"]
            print("获取 WeChar token successful")

            url = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token={}".format(self._access_token)
            res = requests.post(url=get_url, data={})
            print(res.text)

        except Exception as e:
            print("WeChat _get_access_token error:{0}, line number:{1}".format(str(e), e.__traceback__.tb_lineno))
            return False
        return True


if __name__ == "__main__":
    abc = WeChat()
    if (abc._get_access_token()):
        print("token:", abc._access_token)

本篇的主题

  1. axios的使用

  2. puppeteer自动化

  3. docker的初识

  4. api服务器的部署

简介

在上次完成了服务器收发消息功能后,本打算做一个小案例实现如下功能,
当我输入特定文字的时候,服务器返回我需要的文字。
听起来就这么简单,但是万万没想到,这个小案例竟遇到了数不清的坑,让这个项目变得不简单,前前后后花了1天半的时间总算完成初步的实现。本来是想一笔带过的测试,现在变成能够单独写一篇出来,下面就让我来娓娓道来。

最初的想法

因为是做个api所以我一开始就决定在本地先测试,完成后再传到服务器,觉得1个小时差不多就能够搞定。最初的想法:

  • 做成一个模块,当用户输入特定文字后,调用这个模块来获得数据。

  • 需要的数据在某个网站上,所以用axios发送请求去获取页面

  • 解析页面的元素获取要的数据返回给服务器

  • 服务器收到需要的数据后发回给用户

尝试axios获取

axios收发请求我用过很多次,得心应手了,没想到这次就翻车了。安装axios是必要的,axios的中文文档: http://www.axios-js.com/zh-cn/docs/

npm install axios

先看下我的代码:

//getDailyVerse.js
module.exports = () =>{
	return new Promise((reslove, reject) => {
		const axios = require('axios')
		axios.get('https://www.bible.com/zh-CN/verse-of-the-day').then(res => {
			reslove(res.data)
		})
	}) 
	
}

上述代码在遇到80%的网站都能正常解析html页面,但如果网页是前端渲染的话,对不起,你拿不到想要的数据。在这个案例中,恰好我碰到了20%的情况,而且那20%不是前端渲染的问题,而是屏蔽爬虫的问题,当网站检测到你是爬虫后我直接被403没有权限访问了。
我想权限的问题好说,在axios请求加一个请求头来模拟浏览器应该能解决,于是修改下代码, 在请求中添加个请求设置,在设置中添加headers:

//getDailyVerse.js
//...
axios.get('https://www.bible.com/zh-CN/verse-of-the-day', {
	headers: {
		'user-agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36`
		}
	}).then(res => {
		reslove(res.data)
		})
//...

尝试重新运行发现被403,我开始怀疑网站不仅仅是检测浏览器吗?,在我尝试伪造cookie等字段完全没起作用后,又尝试使用node原生的https来访问,node文档: http://nodejs.cn/api/https.html#httpsgeturl-options-callback

//getDailyVerse.js
const https = require('https')
    https.get(require('./config').dailyVerseLink,{
        headers: {
            'user-agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36`
        }
    }, res =>{
        res.on('data',data=>{
            console.log(data.toString())
        }).on('end', ()=>{
            console.log(res.statusCode)
        })
    })



同样也不行,我放弃了,准备使用方案二。

如果有哪个大神在这里能有新的突破请联系我。

使用puppeteer模拟浏览器访问

puppeteer是我之前使用自动化浏览器的库,同样js能有puppeteer写爬虫软件也不输python,我的计划就是使用puppeteer代替axios,其余的方法都一样。
puppeteer文档: https://zhaoqize.github.io/puppeteer-api-zh_CN/

  1. 安装puppeteer

npm install puppeteer
  1. 连接代码

//getDailyVerse.js
module.exports = async() => {
    let data = ''
    const puppeteer = require('puppeteer')
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    //这里把网址写入配置文件中,就不会被上传到git上,也方便修改
    await page.goto(require('./config').dailyVerseLink)
    await page.screenshot({path: './puppeteer.png'})
    await browser.close();
    return data
}

以上代码会在工程目录下生成一个截图,查看截图发现果然被屏蔽了。


解决这种问题这里有两个方案,第一个是不要使用无头浏览器,另一个我先不表。我先选择了第一个方案,没想到这是后续噩梦的开始。

  1. 解决puppeteer屏蔽的方法一
    设置有头浏览器的方法就是一句话,修改下代码:

//getDailyVerse.js
//...
//取消无头设置
const browser = await puppeteer.launch({
	headless: false 
})
//...



果然成功了。

  1. 获取元素
    先在chrome打开网页使用“开发者工具”来定位需要的元素的标签,然后获取里面的文字,代码就直接在这里写完全了:


//getDailyVerse.js
module.exports = async() => {
    let data = ''
    const puppeteer = require('puppeteer')
    const browser = await puppeteer.launch({ 
        headless: false
    })
    const page = await browser.newPage()
    //这里把网址写入配置文件中,就不会被上传到git上,也方便修改
    await page.goto(require('./config').dailyVerseLink)
    //使用$eval选择器定位class类是‘.verse-wrapper.ml1.mr1.mt4.mb4’的元素,然后获取该元素内部的文字
    data = await page.$eval('.verse-wrapper.ml1.mr1.mt4.mb4', el => el.textContent)
    await page.screenshot({path: './puppeteer.png'})
    //打印测试
    console.log(data)
    await browser.close();
    return data
}

运行一下,成功!但是我却忘记了一个大问题,这个是要跑在服务器上的。

这里有个小提示,因为浏览器运行后每次必要关掉,但关掉后数据就没有了,所以我在代码最前面let声明了data的字符串,当获得数据后就保存在其中,关闭浏览器后再return数据也就不丢失了。



新的问题

因为代码不大,只要添加一个文件我就直接使用vscode连接服务器(如何使用vscode远程连接服务器,请参考之前的文章:https://ivanccc.com/archives/gong-zhong-hao-1 )同样的测试,额。。。出问题了,给大家思考下问题出在哪里。



问题就是我这个系统是没有界面的,如何能在linux上使用有界面的浏览器呢。
这个问题很头疼,我花了一整天时间查阅了各种文档资料,尝试数十种方法都无法解决后认为这又是一条死路。
此处省略一万字。。。。。。

关于这个问题,如果有哪个大神知道解决方案请联系我。

我重启原来的方法,还是使用无头浏览器,把headless设置为true后看看能不能使用。额。。。这回是新的错误,错误中也提示了解决的文档:https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md



意思是不能够在root下使用sandbox环境,这里按照文档加上一个设置即可:

//getDailyVerse.js
//...
//取消无头设置,设置no-sandbox环境
const browser = await puppeteer.launch({
	headless: false,
	args: ['--no-sandbox']
})
//...

新的错误又来了,这个错误是puppeteer包本身的内部错误



我解决不了。。。

使用docker解决问题方法

想了一晚,重新查看官方提供解决方案中的内容,看到了一个技术docker。
决定尝试新的方案如下:

  • 找个能运行puppeteer的稳定镜像

  • 在这个合适的镜像生成实例,然后在其上测试代码

  • 成功后设置一个端口映射,所以这次的方法不仅仅只是一个模块,还需要升级成为一个服务

  • 使用项目来访问这个端口实现

我遇到的这个问题其实是环境引起的,所以如果能用配置完成的环境是最省力的方案。

docker介入

https://www.docker.com/

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。
  1. 安装docker
    我直接在服务器宝塔面板中打开软件商店搜索docker并安装


  1. 注册账号
    在上面提供的官网链接中注册账号,完成后转到 https://hub.docker.com/
    这里可以搜索需要的镜像,和查看镜像的版本号


这里我尝试了一些puppeter的镜像,花了半天的时间总算找到合适的环境来运行我这个服务,在此基础上安装了需要的其余依赖和环境,生成了满足需求的镜像,大家需要的话直接下载就可以了。
此处继续省略一万字。。。。。。

  1. 镜像简介
    镜像名:ayachensiyuan/puppeteer, 镜像开放了3001端口作为镜像服务端,使用方法是直接http发起‘GET’请求’/api/getDailyVerse‘即可收到数据。一键安装,真香。。。

这里直接在浏览器查看需要提前把端口加入安全组,同时在宝塔安全设置中开放端口,测试完成记得关闭端口。图片中我是在本地环境运行。)



  1. 安装镜像
    使用termius打开并连接远程服务器,使用以下命令下载镜像(前提是在宝塔中安装好了docker)

docker search ayachensiyuan/puppeteer
docker pull ayachensiyuan/puppeteer:1.3
docker images
  • docker search name —— 该命令和在网站上搜索的是一个效果

  • docker pull name:label —— 该命令就是下载该镜像,label是版本号,如果不指定就是默认latest

  • docker images —— 列出你下载的所有镜像,在宝塔面板中也可查看



docker run -id --name=bibleAPI -p 3001:3001 centos:7 node /root/dailyBible/app.js
  • docker run <参数> <镜像> <命令> —— 运行镜像

    • –name 容器的名字,不指定的话会自动生成奇怪的名字

    • -id 后台运行容器

    • -v 可以设置数据卷,持久化储存数据(具体参看文档)

    • -p设置映射端口,这里设置了服务器的3001对应容器的3001

    • <命令> 进入容器后执行的操作,这里是启动node客户端操作


  1. docker其他命令
    在容器中使用exit命令就可退出容器,如果是-it进入的话,退出的同时就会终止运行容器,其他容器、镜像相关的命令有:

  • docker ps -a —— 查看所以容器(包括停止的,不加-a参数是查看运行的容器)

  • docker exec —— 进入运行的容器

  • docker stop —— 停止容器

  • docker start —— 运行容器

  • docker rm —— 删除容器(需停止)

  • docker rmi —— 删除镜像(需先删除对应容器)

  • docker inspect —— 查看容器

  • docker commit yourDockerName:label —— 容器生成镜像

  • docker save -o <imageName.tar> imageName:label —— 镜像打包

  • docker load -i <imageName.tar> —— 镜像解压生成

  1. 镜像答疑
    有兴趣的小伙伴可以使用以下命令进入镜像查看代码:

docker exec -it bibleAPI /bin/bash

项目目录在’/root/dailyBible/‘中



看一下getDailyVerse.js中关键代码

//getDailyVerse.js
module.exports = async() => {
    let data = ''
    const puppeteer = require('puppeteer')
    const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox']
    })
    const page = await browser.newPage()
    await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4181.9 Safari/537.36")
    await page.goto(require('./config').dailyVerseLink)
    data = await page.$eval('.verse-wrapper.ml1.mr1.mt4.mb4', el => el.textContent)
    await page.screenshot({path: './puppeteer.png'})
    await browser.close();
    return data
}

之前说绕过浏览器检测一个方法是把headless设置为false,但者会产生linux环境下无法启动的问题,所以另一个方式还是请求头的问题,puppeteer这里是在页面goto语句之前就先把“user-agent”设置好,加上这句话:

await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4181.9 Safari/537.36")

原来网站还是由于请求头的“user-agent”的原因被屏蔽,所以我有点疑问,为什么在axios和https中设置headers都不起作用?不知道有人能解答下吗?

服务器api设置

服务器的项目在之前的文章中有介绍,如有疑问可以查看页首链接跳转。
当你在服务器上安装上面提供的命令运行了ayachensiyuan/puppeteer:1.3的镜像容器后,先在宝塔面板的网站选项卡中停止node项目


之后使用vscode远程连接到服务器工程目录,修改app.js中POST’/wx’的RESTAPI。

之前代码有个问题也一并解决,就是我仅仅设置消息类型为text的解析,没有考虑到其他类型,如果当用户关注或取消关注这类消息的类型没有content字段,服务器解析时候就会崩溃,所以这里消息处理仅仅针对文字类型的消息,其他类型先不用理会。
//getDailyVerse.js
//...
//接受消息并做出反应
app.post('/wx', async (req, res) => {
    const xmlMsg = await require('./getUserMessage')(req)
    const jsonData = await require('./paresMsg')(xmlMsg)
    //教学环境做个简单的处理
    if (jsonData.MsgType == 'text') {
        //发送每日经文
        if (jsonData.Content[0] == '每日经文') {
            const axios = require('axios')
            //使用axios来处理请求
            axios.get('http://127.0.0.1:3001/api/getDailyVerse').then(response => {
                //使用sendMsg方法发回收到的数据
	        //解析数据都是数组,所以需要[0]获取
                const xmlSendData = require('./sendMsg')(jsonData.FromUserName[0], jsonData.ToUserName[0], response.data)
                res.send(xmlSendData)
            })
        } else {
            //其余文字还是暂时以倒转文字返回
            const xmlSendData = require('./sendMsg')(jsonData.FromUserName[0], jsonData.ToUserName[0], jsonData.Content[0].split('').reverse().join(''))
            //const xmlSendData = require('./jsToXml')(sendData)
            //console.log(xmlSendData)
            res.send(xmlSendData)
        }
    } else {
        //暂时目前无需处理其他类型数据
        res.end('')
    }
})
//...
//parseMsg.js
const xml2js = require('xml2js')
const xmlParser = new xml2js.Parser()
module.exports = parseMsg = (xmlmsg)=>{
    return new Promise((reslove,reject)=>{
        xmlParser.parseString(xmlmsg,(err,res)=>{
            // console.log(res)
            const callbackData = {
                ToUserName: res.xml.ToUserName,
                FromUserName: res.xml.FromUserName,
                MsgType: res.xml.MsgType,
                CreateTime: res.xml.CreateTime,
                Content: res.xml.Content,
                MsgId: res.xml.MsgId
            }
            reslove(callbackData)
        })

    })
}

测试一下:


总结

项目其实并不复杂,但过程还是走了很多弯路的,尤其在制作docker镜像期间尝试了很多镜像,最终找到适合puppeteer的环境。随着功能越来越多app.js的文件也变得庞大,下次的更新就整理分离代码,按照业务逻辑去规划你的代码。请继续关注。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:urlencode,什么是urlencode编码
下一篇:比利时淘汰卫冕冠军,捷克爆冷击退荷兰!
相关文章

 发表评论

暂时没有评论,来抢沙发吧~