简介:这是一份面向Python开发者与企业微信管理员的消息通知自动化工具包,解决了通过API向企业成员或外部联系人批量推送文本、Markdown及图文消息的需求。资源包仅包含1个Python脚本文件,压缩包大小约2KB,轻量易用,适合有基础Python知识、希望快速接入企业微信机器人接口的学习者或运维人员。脚本内封装了send_text_msg、send_markdown_msg、send_image_text_msg三个函数,分别对应文本、Markdown和图文消息的发送,核心逻辑基于requests库实现POST请求,调用简单,可直接复制到现有监控报警、数据报告或运营通知流程中。目前已超过7500人学习下载,说明该脚本具有较高的实用参考价值。通过这份资源,读者可以快速掌握企业微信机器人的调用方式,节省查阅官方文档和调试的时间,并以此为模板扩展更多自定义消息类型或集成到定时任务中,提升企业内部信息传递效率。
1. 企业微信群机器人:一条 Webhook 消息的后半程
如果你以为「企业微信机器人」必须装官方 SDK、配回调服务才能玩,那这个工程会给你一个反直觉的答案:整个项目的核心,其实只是往一个 Webhook 地址上 POST JSON 数据。半夜告警电话打过来的时候,我最常干的事不是爬起来看监控面板,而是让 Python 脚本把堆栈和错误码直接丢进群里——这套 Python 实现企业微信群机器人消息发送的源码包,就是干这个用的。它适合运维、后端、爬虫和数据运营,能把文本、Markdown、图片、图文卡片一键推到群聊,不需要服务器常驻,不需要回调端口,一段脚本就是一条生产线。
2. 技术选型与工程结构:为什么不装 SDK,只用标准库
2.1 选型逻辑:Webhook 模式决定了请求复杂度
企业微信群的机器人对外只暴露一个 HTTP 接口,所有消息类型最终都是同一个 URL 上的不同 JSON 结构。这意味着你不需要 SDK,不需要维护长连接,也不需要理解企业微信内部复杂的会话协议。整个技术栈被压缩到了最小:requests负责 HTTP 传输,hashlib、hmac、base64负责加签场景的签名计算,argparse负责命令行入口。剩下的全是数据组装逻辑。
有人会问,那urllib不也能跑吗?能,但 requests 在超时控制、连接复用、异常信息上更直观。尤其当你准备把发送函数嵌入业务系统时,timeout=3这种参数能救你命——这后面避坑章节会专门讲。这个工程不引入 Flask、Celery 之类的东西,因为它本质是「发送即走」的模型,和公众号那种需要接收事件的模式完全不同。
2.2 常见的包内结构与调用链路
这类源码包解压后,结构通常是这样几个文件各司其职:
| 文件 | 职责 |
|---|---|
send_msg.py | 消息类型组装与发送入口,对外暴露send_text/send_markdown/send_image |
upload_media.py | 图片、文件先走临时素材上传,返回media_id |
config_sample.py | 存放 Webhook Key、加签 Secret、默认超时时间 |
requirements.txt | 依赖清单,实际只有requests |
README.md | 使用说明和参数限制表 |
调用关系是一层套一层:你在脚本里调send_text("hello"),它内部把文本包装成{"msgtype":"text","text":{...}},然后统一交给一个私有_post方法拼接完整 URL、发起 POST、解析返回码。图片消息特殊一点,得先调upload_media.py把文件传上去换回media_id,再用相同入口发送。这样设计的好处是,以后想加投票、文件、模板卡片这类消息,只需新增一个组装函数,底层的请求与异常处理完全不用动。
2.3 一条消息从进入到返回的完整链路
整套流程跑一次,在代码层面经过四个阶段。第一阶段是参数校验,content长度、文件大小、msgtype合法性都在这里过滤;第二阶段是 URL 拼接,Webhook 地址末尾带key=xxx,如果群机器人开启了加签,还会追加timestamp和sign两个查询参数;第三阶段是 POST 发送并等待响应,一般 1 到 3 秒;第四阶段是返回码判断,errcode=0算成功,非 0 直接抛带错误码的异常。
很多第一次接触的人会在这个链路里翻车:有人把key当成 access_token,有人把 Webhook 地址填成企业微信 API 的基础域名,还有人忽略了响应里errcode之外的冗余字段。这个工程在_post方法里统一处理了这些歧义,你调函数时根本不需要关心 URL 长什么样,只需要知道「成功就是errcode=0,失败就会抛异常」。
3. 核心模块拆解:文本、Markdown、图片与签名实现
3.1 文本消息:content 长度和 @ 人的两个隐藏细节
文本消息是使用频率最高的类型,代码实现也最直接:
import requests WEBHOOK_KEY = "your-key-here" def _post(data: dict) -> dict: """统一的发送入口,非 0 返回码一律抛异常。""" url = ( f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send" f"?key={WEBHOOK_KEY}" ) resp = requests.post(url, json=data, timeout=3) result = resp.json() if result.get("errcode") != 0: raise RuntimeError( f"发送失败: errcode={result.get('errcode')}, " f"errmsg={result.get('errmsg')}" ) return result def send_text(content: str, mentioned_list: list = None) -> dict: """发送纯文本消息。mentioned_list 传成员 userid。""" payload = { "msgtype": "text", "text": { "content": content, "mentioned_list": mentioned_list or [] } } return _post(payload)逻辑说明:_post是整套脚本的心脏,所有消息类型最后都从这里出去。timeout=3是硬性要求,没有超时的请求在故障时会把业务线程拖死。send_text里的content上限是 2048 字节,注意是字节不是字符,中文内容一多就踩线;mentioned_list如果留空并且不带mentioned_mobile_list,那就只是普通消息,想 @所有人 得显式传["@all"]。
3.2 Markdown 消息:子集语法与 @ 的另类写法
很多人以为 Markdown 模式和文本一样,只是换个msgtype,实际用起来会发现两个坑:第一,它支持的是官方规定的子集,不是完整 GFM,表格和数学公式直接不渲染;第二,@ 人语法和文本模式完全不一样,必须写在正文里,形如<@userid>。
def send_markdown(content: str) -> dict: """发送 Markdown 消息,content 上限 4096 字节。""" payload = { "msgtype": "markdown", "markdown": {"content": content} } return _post(payload) # 使用示例 send_markdown("## 发布通知\n版本 **v2.3.1** 已上线\n<@zhangsan> 请验收")参数说明:content里支持标题、加粗、链接、引用、颜色字体,但颜色字体只对<font color="info">这类预设值生效。@ 人这里非常容易写错,我见过有人把<@zhangsan>当成文本消息的mentioned_list用,结果群里毫无反应。区分标准很简单:文本模式用mentioned_list数组,Markdown 模式靠正文中的<@userid>标签。
3.3 图片消息:临时素材上传背后的 base64 与 md5
图片消息不能直接发图片 URL,必须先把图片文件上传到企业微信临时素材库,换回media_id,再用图片消息类型发送。上传接口返回的media_id有效期一般是 3 天,足够日常告警场景使用。
def upload_image(image_path: str) -> str: """上传图片到临时素材库,返回 media_id。""" upload_url = ( f"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media" f"?key={WEBHOOK_KEY}&type=image" ) with open(image_path, "rb") as f: resp = requests.post( upload_url, files={"media": (image_path, f, "image/jpeg")}, timeout=5 ) data = resp.json() if data.get("errcode") != 0: raise RuntimeError( f"上传失败: {data.get('errmsg')}" ) return data["media_id"] def send_image(media_id: str) -> dict: payload = {"msgtype": "image", "image": {"media_id": media_id}} return _post(payload)这段代码里两个点值得说。一是files参数必须显式给media这个字段名,企业微信接口认的是media而不是file,拼错直接返回invalid media type。二是图片本身限制在 2MB 以内、支持 jpg/png,但有些客户端截屏生成的图片动辄 3MB,我一般会在上传前加一步 Pillow 压缩;如果不想引入 Pillow,还有一个土办法——把图片读到内存转 base64,再调上传接口,但 base64 会增加 33% 体积,反而更容易超限,所以这只是糊弄方案,正经做还是得压缩。
3.4 加签 Webhook:HMAC-SHA256 的签名计算
群机器人的安全设置里有三种:关键字、IP 段、加签。加签最麻烦但也最稳,它要求请求 URL 额外带上timestamp和sign参数,服务器用同一套算法校验身份。
import hashlib import hmac import base64 import time def _generate_sign(secret: str, timestamp: str) -> str: """计算签名: HmacSHA256(timestamp + '\\n' + secret) 后做 base64。""" string_to_sign = f"{timestamp}\n{secret}" hmac_code = hmac.new( secret.encode("utf-8"), string_to_sign.encode("utf-8"), digestmod=hashlib.sha256 ).digest() return base64.b64encode(hmac_code).decode("utf-8") def send_signed(data: dict, secret: str) -> dict: timestamp = str(int(time.time())) sign = _generate_sign(secret, timestamp) url = ( f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send" f"?key={WEBHOOK_KEY}×tamp={timestamp}&sign={sign}" ) resp = requests.post(url, json=data, timeout=3) result = resp.json() if result.get("errcode") != 0: raise RuntimeError(f"签名发送失败: {result}") return result逻辑说明:签名字符串的拼接顺序必须是timestamp\nsecret,这个换行符不能丢,丢了签名永远对不上。timestamp取的是秒级 Unix 时间戳,不是毫秒。算法是标准HmacSHA256,算完再用 base64 编码。我见过有人在这里用hashlib.sha256直接算,结果返回 93002 签名错误——血泪经验是:先看是不是 get 参数没拼到 URL 里,再看拼接串顺序,最后才怀疑算法实现。
4. 跑通全流程:从机器人配置到定时告警联动
4.1 三个安全选项怎么选
先到群聊设置里添加群机器人,系统会给你一个 Webhook 地址,长这样:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx地址里的key就是身份凭证。安全设置三个选项,我的建议是:纯粹发告警就开「关键字」+「IP 段」,关键字设成告警、故障这类业务词,能挡掉误触发的陌生人;如果机器人部署在动态 IP 机房,就加签,这样只要密钥不泄露,从哪发都行。注意这三个设置可以叠加,但要小心关键字和内容冲突——你发的每条消息都得带上命中的关键字,否则机器人会罢工,这个坑后面专门讲。
4.2 一条命令跑通第一发消息
源码包里通常已经带了一个命令行入口,把config_sample.py改成你自己的config.py,填入 Webhook key,然后执行:
python send_msg.py --type text --content "构建完成,可以开始测试" python send_msg.py --type markdown --content "## 上线通知\n版本 **v2.3.1** 已部署" python send_msg.py --type image --path /tmp/screenshot.png这里send_msg.py内部用argparse接收参数,--type决定走哪个组装函数。我拿到这类工程的第一件事,是先跑一条纯文本,确认errcode=0,再测图片,最后才接业务逻辑。不要上来就把脚本嵌到正式系统里,万一 Webhook 地址填错,报错信息会被业务代码吞掉,排查成本翻倍。
4.3 接进定时任务与异常告警
跑通后最常用的场景是定时日报和异常告警。用 crontab 就能搞定:
0 9 * * * cd /opt/wecom-bot && /usr/bin/python3 send_msg.py --type text --content "每日数据报表已生成,请查收" * * * * * /opt/wecom-bot/health_check.sh || /usr/bin/python3 /opt/wecom-bot/send_msg.py --type text --content "告警: 服务健康检查失败"第二个定时任务里,health_check.sh退出码非 0 时触发告警,这是最简单的联动。更进阶的做法是在业务代码里捕获异常后把 traceback 传进来:
try: run_job() except Exception as exc: import traceback send_markdown(f"## 任务异常\n```\n{traceback.format_exc()}\n```") raise这样群里看到的不是冷冰冰的「失败」,而是完整堆栈,定位问题可以省一半时间。
4.4 看响应码定位问题
企业微信返回的errcode基本能告诉你所有答案,常用的几个:
| errcode | 含义 | 处理方式 |
|---|---|---|
| 0 | 成功 | 无需处理 |
| 93000 | 机器人已被移除群聊 | 重新添加机器人并替换 key |
| 93001 | 群已解散 | 换群 |
| 93002 | 签名不匹配 | 检查 timestamp 和 sign 拼接 |
| 93003 | 消息类型或格式错误 | 对照各 msgtype 的字段要求 |
| 93004 | 内容超过长度限制 | 裁剪后分段发送 |
| 93005 | 内容包含违规词 | 去掉命中词或换措辞 |
我一般会在_post里把整个返回体打日志,只看errcode容易漏信息,比如 93005 的errmsg会直接告诉你哪个词违规,这对后续做内容清洗很有用。
5. 避坑专题:拦截、签名失败、超时与 @ 失效的排查记录
5.1 消息发出去了,群里却看不到
现象:errcode=0,接口返回成功,但群聊里根本没有这条消息。
原因:这个情况十有八九是触发了「关键字」安全设置。比如机器人配置了关键字「告警」,而你发送的文本内容里没有包含「告警」两个字,企业微信会在服务端静默丢弃,但为了安全不返回错误码。另外还有一种可能是机器人被群主移除了,这种会返回 93000,比较容易发现。
解决:先回群里确认机器人是否还在,再看安全设置命中了哪种规则。发消息时保证内容携带关键字,或者干脆用仅 IP 段模式。
5.2 图片上传返回 invalid media type
现象:调用上传接口总是报 media 类型不合法,换 jpg、png 都不行。
原因:一多半是files字段名写错了,接口要求的字段名是media,不是file。还有一部分是上传接口的 URL 末尾漏了&type=image,导致服务端不知道你要传什么类型的素材。
解决:先核对字段名和 URL 参数。如果都正确,大概率是图片本身的问题——检查是不是大小超过 2MB,或者其实是 webp 格式但扩展名改成了 png。这个我用过一个笨办法:用PIL.Image.open重新打开图片并转成 RGB 再保存为 jpg,能解决 90% 的格式玄学问题。
5.3 Markdown 里的 @ 人永远没生效
现象:文本消息里用mentioned_list能正常 @,但切到 Markdown 消息后,怎么传数组都没反应。
原因:Markdown 消息类型根本不认识mentioned_list字段,它只解析正文里的<@userid>标签。很多人错误地沿用文本消息的写法,把 @ 目标写在字段里,自然不生效。
解决:在 Markdown 正文中拼接<@userid>。比如<@zhangsan> 请确认这个发布单,一个 userid 对应一个标签。注意这里的 userid 是成员的企业微信账号,不是手机号,也不是昵称。
5.4 加签机器人一直报 93002
现象:开了加签后,之前正常发送的脚本全部返回 93002 签名错误。
原因:三个高频出错点。第一,签名字符串拼接顺序错了,必须严格按timestamp\nsecret来,换行符被 trim 掉是最常见的;第二,timestamp传成了毫秒级,正确是秒级;第三,sign没有做 URL 编码,base64 结果里的+号在 URL 里被解释成空格,直接导致签名校验失败。
解决:requests.get或直接拼 URL 时,对 sign 做一次urllib.parse.quote,或者在拼 URL 时用params参数让 requests 自己编码。我当时在这个坑里耗了半小时,最后打印出实际请求 URL 才发现+变空格了。
5.5 业务线程卡死在 requests.post 上
现象:脚本本身没问题,但一旦企业微信接口超时,整个业务进程就卡住,后面的任务全部排队。
原因:代码里没有设置timeout,或者设置的太大。requests.post默认不设超时就是无限等待,TCP 层可能挂几分钟才报错。
解决:所有 HTTP 请求统一加timeout=3。如果追求更稳,可以再加retries:失败一次后间隔 0.5 秒重试,最多 2 次,还不行就丢弃消息并记录日志——告警通道不能反过来成为业务故障源,这是我在生产环境用血的教训换来的原则。
6. 二次开发:把机器人扩展成一套通用通知服务
6.1 封装成 Notifier 类,收敛超时与重试
源码包里的函数式调用适合快速跑通,真要嵌入多个业务系统,我会把它重构成类:
class WeComNotifier: """企业微信机器人通知器,支持重试与多群分发。""" def __init__(self, key: str, secret: str = None, timeout: int = 3, retries: int = 2): self.key = key self.secret = secret self.timeout = timeout self.retries = retries def _post(self, data: dict) -> dict: for attempt in range(self.retries + 1): try: return self._do_post(data) except requests.RequestException: if attempt == self.retries: raise time.sleep(0.5 * (attempt + 1)) def send(self, msgtype: str, content: str, **kwargs): payload = {msgtype: {"content": content, **kwargs}} return self._post({"msgtype": msgtype, **payload})重构思路是把 URL 拼接、签名、超时、重试全部下沉到_post,业务方只需要notifier.send("text", "disk 使用率 95%")。send方法我故意做得灵活一点,让调用方可以直接传mentioned_list等扩展字段。
6.2 多群分发与降级策略
实际项目里往往有「告警群」「开发群」「运营群」好几个群,每个群 key 不同。维护一个群名 -> key的配置字典,初始化多个WeComNotifier实例,发消息时按群名路由。降级策略也很关键:企业微信不可用时,把同样内容写到本地日志文件,或者调用备用通道(比如邮件接口),保证通知不丢。
6.3 可观测性的最小实现
给_post加一个耗时统计装饰器,每次发送把errcode、耗时、消息类型写入结构化日志:
import logging def log_send(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) logging.info( "wecom_send msgtype=%s cost=%.3f errcode=%s", kwargs.get("msgtype"), time.perf_counter() - start, result.get("errcode") ) return result return wrapper验证方法很朴素:先用--type text发一条「测试消息」,确认群里收到;再用--type image发一张超过 2MB 的图,确认报错信息准确;最后停掉网络模拟失败场景,确认重试生效,整个改造就算验收完成了。
我自己的习惯是,任何系统的通知模块都必须能单独开关、单独调级别。有一回我在业务代码里直接调 requests.post 发告警,没设 timeout,结果企业微信服务器抖动,发送线程把消息队列堵了,连带正常业务流程也慢下来——那是彻底翻车的一次。从那以后,我每次加通知代码,都强制走一遍「超时、重试、降级日志」这三件套,再小的工具也不偷懒。希望帮到你。
本文还有配套的精品资源,点击获取