惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
K
Kaspersky official blog
T
The Exploit Database - CXSecurity.com
NISL@THU
NISL@THU
AWS News Blog
AWS News Blog
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Security @ Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Commits to openclaw:main
Recent Commits to openclaw:main
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
Attack and Defense Labs
Attack and Defense Labs
Jina AI
Jina AI
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
H
Help Net Security
V
Visual Studio Blog
宝玉的分享
宝玉的分享
C
Cybersecurity and Infrastructure Security Agency CISA
T
Threat Research - Cisco Blogs
IT之家
IT之家
Hugging Face - Blog
Hugging Face - Blog
Latest news
Latest news
T
Tor Project blog
I
Intezer
美团技术团队
GbyAI
GbyAI
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
Google DeepMind News
Google DeepMind News
Scott Helme
Scott Helme
Y
Y Combinator Blog
博客园 - 司徒正美
T
Tenable Blog
O
OpenAI News
N
News and Events Feed by Topic
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
Vulnerabilities – Threatpost
P
Palo Alto Networks Blog
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Threatpost
Google Online Security Blog
Google Online Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
Help Net Security
Help Net Security

mephisto.cc

修复rime-ice无法弹出候选框的问题 开机滚动字体显示优化 个人网站安全防护 树莓派上部署Vaultwarden 使用goaccess实时分析Caddy日志 个人网站监控 Hugo全站AVIF记 Linux下尝试使用Godot开发小游戏 Arch linux dae 透明代理 Airflow接管galler-dl下载任务 如何使用gallery-dl批量下载图像 Arch核显下如何愉快玩Dota2和CS2 Arch/labwc 环境网络相关设置 Rime添加dota2词库 Arch 重装记录 Supertuxkart iOS版终于发布了 打造自己的流媒体音乐服务 剪切板管理工具clipcat推荐 MangoHud性能监控 微信小程序开发记 Rofi试用 解决Arch下VSCode无法输入中文的问题 独立窗口管理器下无法录屏问题处理 上海市二手房成交数据监控 OpenLDAP监控 Arch linux如何顺畅连接蓝牙设备 使用Git和Ansible管理配置文件 Arch Linux SSL VPN 客户端配置 简单获取celery任务实时结果 Arch linux下iNode客户端的安装和使用方法 券商费率调整记 迷你主机组装记 Labwc更换定制皮肤 树莓派跑分流代理 实用自动代理配置示例 九年老鼠标复活记 Openvpn示例 Mac下如何旋转webp图片 如何隐藏Vscode中Markdown PDF插件的头部内容 解决Linux下网络配置无法变更保存的问题 labwc环境启用wlogout Ubuntu下新官方微信尝鲜 简单检测用户是否开启广告屏蔽 Atuin ZFS下卡顿问题解决 Wine安装64位微信 Fish 和 Atuin使用记 Fastapi简单实现临时下载链接 Hugo根据语种展示不同内容 如何修复Waybar微信图标错误 信封加密简要 Linux环境下维护公众号记录 快速隐藏和呼出终端 Wayland环境自动切换壁纸 SuperTuxKart 试玩 简单抓取雪球股票快讯 为什么你的kill命令不能列出信号表 Wine安装微信保姆教程 Hysteria科学上网简要 Ubuntu 23.10 钉钉无法启动解决方法 网站导航栏防止插入Adsense自动广告 Linux环境按键检测 Hugo文末添加最后修改时间 Wayland环境ksnip无法复制问题解决 我眼中的股票市场 Caddy简单图片防盗链 greetd和regreet使用教程 Wayland环境下截图加后期修改 Wayland 环境下gif录屏 Firefox的一些有趣功能 Ubuntu 切换系统语言 Firefox 标题栏高度调整 网站添加回到顶部功能 Hugo自定义字体 Labwc 便捷配置 Wofi使用教程 窗口管理器labwc使用记 文件共享软件Dufs推荐 Hugo静态站点接入Adsense广告 Fcitx5配置详解 Ubuntu安装chrome的方法总结 一种Xterm.js的全屏方案 Google网址收录api Python示例 手机听离线音乐 Snipe it资产导入 应用启动器yofi使用配置 Snipe it资产管理系统安装使用 Ubuntu开机启动加速 Vue3 vite TypeScript跨域等相关设置 Ubuntu手动升级Libreoffice Ubuntu设置alacritty为默认终端 简体文章批量转换为繁体 使用Inkscape调整svg图片大小 实用命令 使用mitmproxy给手机app抓包 Python中rstrip方法细解 Github同主机多仓库部署deploy key问题处理 Linux安装最新版本Python KeePassXC使用教程 Ubuntu 22.10连接蓝牙耳机报错br-connection-profile-unavailable解决方法 Caddy日志配置轮转和格式化
Fastapi微信公众号开发简要
2024-01-19 · via mephisto.cc

去年底申请了个微信公众号,网站更新的时候,顺带更新微信公众号,体验有好有坏。

坏的方面:

  • 那个编辑文章的后台,不支持原生 markdown 文件;
  • 文章发布后,不能像网站那样灵活更改了。

好的方面:

  • 微信用户多,开流量主能搞钱,能不能火起来就看你有多大本领了,我这种站点基本不可能发达;
  • 能在公众号号平台开发额外功能,方便自己也能为用户提供一些轻量服务。

本文主要讲如何使用 Python 进行简单微信公众号开发,内容点为:

  • 文本自动回复

  • 关注公众号自动回复

  • 文本换行实现

wechat-return1

框架我选的 Fastapi,因为我的一个站点的后端实现的为 Fastapi,包括网站的自动更新逻辑也是。

且公众号主要是 api 交互,Fastapi 写 api 简单快速。

怎么注册公众号,绑定服务器就不说了,网上一大堆,没必要再写一份,直接上代码节约时间。

友情提示:代码不一定能跑起来,因为很多网站业务相关敏感信息删掉了,也懒得费力去测试,只展示了重要的部分,代码不见得漂亮但大部分是在线上跑的。

1. 微信签名验证

微信有自己的一套签名算法,证明连的服务器是对的,不然的话,连到一个黑服务器,乱搞一通,岂不天下大乱。只有通过签名认证,表明你是正确的服务提供方,才能做后续的内容服务。

 1
 2from fastapi import BackgroundTasks, FastAPI, Response, Request
 3import hashlib
 4import logging
 5
 6import reply
 7import receive
 8
 9WX_TOKEN = "your_token" #你的token,微信后台里面填的。
10
11logger = logging.getLogger("__name__")
12
13app = FastAPI()
14
15@app.get("/api/wx")
16async def handle_wx(signature, timestamp, nonce, echostr):
17    try:
18        wx_params= [WX_TOKEN, timestamp, nonce]
19        wx_params.sort()
20        hashcode = hashlib.sha1("".join(wx_params).encode('utf-8')).hexdigest()
21        logger.info(f"hashcode:{hashcode},signature:{signature}")
22        if hashcode == signature:
23            logger.info("验证成功")
24            return int(echostr)
25        else:
26            logger.error("加密字符串 不等于 微信返回字符串,验证失败!!!")
27            return "验证失败!"
28    except Exception as error:
29        return f"微信服务器配置验证出现异常:{error}"

这里不得不说,微信官方 webpy 文档还是 python2 的写法,早就过时了。反正你按照自己理解,不停测试验证通过了就行。

2. 文本回复

 1
 2@app.post("/api/wx")
 3async def handle_wx(request: Request):
 4    try:
 5        webData = await request.body()  # Fastapi下获取请求body, 和webpy不一样哦,毕竟框架不一样,写法就不一样,牛吃草,人吃饭。
 6        print("Handle Post webdata is ", webData)
 7        recMsg = receive.parse_xml(webData)  # 交给receive.py去解析
 8        toUser = recMsg.FromUserName
 9        fromUser = recMsg.ToUserName
10        if isinstance(recMsg, receive.Msg):
11            Content = recMsg.Content
12            Content = Content.decode("utf-8")
13            if recMsg.MsgType == 'text':
14                if Content == "芝麻开门": # 这里是给wine微信的用户提供关键文件下载的(雷锋附体,真正的为人民服务), 真代码不贴了。
15                    pass
16                elif Content.isnumeric() and len(Content)==6: # 这里是股票名单相关,返回缅A信息,读者不用关心。
17                    print("content:", Content)
18                    logger.info("查询列表中...")
19                    content = await check_stock(Content)
20                    logger.info("开始发送列表信息...")
21                    replyMsg = reply.TextMsg(toUser, fromUser, content)
22                    # return replyMsg.send()
23                    # print("reply send:", type(replyMsg.send()))  #这里返回的是字符串类型
24                    return Response(content=replyMsg.send(), media_type="application/xml") # 一定要指定‘application/xml’,不然换行有问题,很关键!!!
25                else:
26                    logger.info("该文本信息没匹配中,暂不处理")
27                    return "success"
28            # 目前没想好需要提供什么图片服务,人穷,小站带宽有限,不处理...
29            elif recMsg.MsgType == 'image':
30                logger.info("该图片信息没匹配中,暂不处理")
31                return "success"
32            else:
33                return reply.Msg().send()
34        elif isinstance(recMsg, receive.EventMsg):   # 订阅和退订属于微信定义的 ‘事件’ 类型
35            # 关注自动回复内容,一般向用户返公众号都有些什么服务,好比饭馆的菜单。当然你有自己的独特思路,当我没说。
36            if recMsg.Event == 'subscribe':
37                logger.info("又有人订阅啦")
38                content = "【🤗终于等到你啦,感谢关注!】\n① 输入6位数缅A代码,可查询是否被标记\n② 输入'芝麻开门'可获得wine微信相关依赖包临时下载地址\n③ 其它功能待开发..."
39                replyMsg = reply.TextMsg(toUser, fromUser, content)
40                return Response(content=replyMsg.send(), media_type="application/xml")
41            # 退订消息实际不发,但是日志还是可以打的
42            if recMsg.Event == 'unsubscribe':
43                logger.info("我们失去了一个伙伴")
44                content = "【🤝后会有期,江湖再见!】\n"
45                replyMsg = reply.TextMsg(toUser, fromUser, content)
46                return Response(content=replyMsg.send(), media_type="application/xml")
47            else:
48                logger.info("该事件没匹配中,暂不处理")
49                return "success"
50        else:
51            logger.info("不是图片、文本、事件,暂不处理")
52            return reply.Msg().send()
53    except Exception as Argment:
54        return Argment

网上搜索微信公众号开发如何换行,会发现很多人处理不了,到处求助,而且回答者大部分不提供明确代码示例,我这个可以说是良心之作了,简单一看就懂。

代码不漂亮,但是它真能换行,有图有真相,其它编程语言思路差不多的。

wechat-return2

一定要指定为信息类型为application/xml,否则当作普通文本,按照微信的规则,是无法换行的。

receive.pyreply.py 和微信官方的差不多,只稍微改动,多写代码多掉头发,能跑的代码不用是傻子 😄

receive.py 文件内容:

 1
 2# -*- coding: utf-8 -*-#
 3# filename: receive.py
 4import xml.etree.ElementTree as ET
 5
 6
 7def parse_xml(web_data):
 8    if len(web_data) == 0:
 9        return None
10    xmlData = ET.fromstring(web_data)
11    msg_type = xmlData.find("MsgType").text
12    if msg_type == "text":
13        return TextMsg(xmlData)
14    elif msg_type == "image":
15        return ImageMsg(xmlData)
16    elif msg_type == 'event':
17        event_type = xmlData.find('Event').text
18        if event_type == 'CLICK':
19            return Click(xmlData)
20        elif event_type in ('subscribe', 'unsubscribe'):
21            return Subscribe(xmlData)
22        #elif event_type == 'VIEW':
23            #return View(xmlData)
24        #elif event_type == 'LOCATION':
25            #return LocationEvent(xmlData)
26        #elif event_type == 'SCAN':
27            #return Scan(xmlData)
28
29
30
31class Msg(object):
32    def __init__(self, xmlData):
33        self.ToUserName = xmlData.find("ToUserName").text
34        self.FromUserName = xmlData.find("FromUserName").text
35        self.CreateTime = xmlData.find("CreateTime").text
36        self.MsgType = xmlData.find("MsgType").text
37        self.MsgId = xmlData.find("MsgId").text
38
39
40class TextMsg(Msg):
41    def __init__(self, xmlData):
42        Msg.__init__(self, xmlData)
43        self.Content = xmlData.find("Content").text.encode("utf-8")
44
45
46class ImageMsg(Msg):
47    def __init__(self, xmlData):
48        Msg.__init__(self, xmlData)
49        self.PicUrl = xmlData.find("PicUrl").text
50        self.MediaId = xmlData.find("MediaId").text
51
52
53class EventMsg(object):
54    def __init__(self, xmlData):
55        self.ToUserName = xmlData.find("ToUserName").text
56        self.FromUserName = xmlData.find("FromUserName").text
57        self.CreateTime = xmlData.find("CreateTime").text
58        self.MsgType = xmlData.find("MsgType").text
59        self.Event = xmlData.find("Event").text
60
61
62class Click(EventMsg):
63    def __init__(self, xmlData):
64        EventMsg.__init__(self, xmlData)
65        self.Eventkey = xmlData.find("EventKey").text
66
67
68class Subscribe(EventMsg):
69    def __init__(self, xmlData):
70        EventMsg.__init__(self, xmlData)
71        self.Eventkey = xmlData.find("EventKey").text

replay.py 文件内容:

 1# -*- coding: utf-8 -*-#
 2# filename: reply.py
 3import time
 4
 5class Msg(object):
 6    def __init__(self):
 7        pass
 8
 9    def send(self):
10        return "success"
11
12class TextMsg(Msg):
13    def __init__(self, toUserName, fromUserName, content):
14        self.__dict = dict()
15        self.__dict['ToUserName'] = toUserName
16        self.__dict['FromUserName'] = fromUserName
17        self.__dict['CreateTime'] = int(time.time())
18        self.__dict['Content'] = content
19
20    def send(self):
21        XmlForm = """
22            <xml>
23                <ToUserName><![CDATA[{ToUserName}]]></ToUserName>
24                <FromUserName><![CDATA[{FromUserName}]]></FromUserName>
25                <CreateTime>{CreateTime}</CreateTime>
26                <MsgType><![CDATA[text]]></MsgType>
27                <Content><![CDATA[{Content}]]></Content>
28            </xml>
29            """
30        return XmlForm.format(**self.__dict)
31
32class ImageMsg(Msg):
33    def __init__(self, toUserName, fromUserName, mediaId):
34        self.__dict = dict()
35        self.__dict['ToUserName'] = toUserName
36        self.__dict['FromUserName'] = fromUserName
37        self.__dict['CreateTime'] = int(time.time())
38        self.__dict['MediaId'] = mediaId
39
40    def send(self):
41        XmlForm = """
42            <xml>
43                <ToUserName><![CDATA[{ToUserName}]]></ToUserName>
44                <FromUserName><![CDATA[{FromUserName}]]></FromUserName>
45                <CreateTime>{CreateTime}</CreateTime>
46                <MsgType><![CDATA[image]]></MsgType>
47                <Image>
48                <MediaId><![CDATA[{MediaId}]]></MediaId>
49                </Image>
50            </xml>
51            """
52        return XmlForm.format(**self.__dict)

上面代码都很简单实用,如果你实在理解不了,可以先搜索下各种文档,再微信公众号联系也可以。

往后如果实现值得分享的功能,再写后续篇章。