bash
QiWX开放平台 · 个人名片
API驱动企微外部群自动化,让开发更高效
官方站点:https://www.qiwx.online
对接通道:进入官方站点联系客服
技术定位:企微生态深度服务,专注 API+RPA 融合技术方案
"用 Python 给企业微信外部群发消息"------这个需求听起来 RPA 就能搞定,但真正放到生产环境里跑,你会发现简单的回复还能勉强实现,复杂点的业务场景很难应付:要么消息发不出去,要么账号被风控,要么并发一上来整个脚本卡死。
这篇文章用 Python 把"外部群主动消息发送"从最朴素的一次调用,一步步写到能扛生产的版本,每一步都说清楚为什么要这么改。
一、最朴素的一版:一次调用长什么样
主动发消息,本质就是一个 HTTP POST,带上三样东西:从哪个节点发、发给谁、发什么。
python
import httpx
GATEWAY = "http://api.qiwx.online/work-weixin/api/doApi"
TOKEN = "your_token" # 应用凭证 X-QIWEI-TOKEN可以在官方站点:https://www.qiwx.online 获取!
def send_text(guid: str, to_id: str, content: str):
resp = httpx.post(
GATEWAY,
headers={
"Content-Type": "application/json",
"X-QIWEI-TOKEN": TOKEN,
},
json={
"method": "/msg/sendText",
"params": {"guid": guid, "toId": to_id, "content": content},
},
timeout=30,
)
return resp.json()
# 发一条
send_text("xxxx-guid", "群id", "您好,这是一条测试消息")
guid:群挂在哪个登录中的企业微信账号下,就用哪个节点的 guidto_id:目标外部群的 idcontent:文本内容
这一版能跑通,但它只适合"发一条玩玩"。下面的问题,全是从"发很多条"开始的。
二、第一个坑:批量发不能写成裸 for 循环
需求一升级------"给 500 个群发一条通知"------新手几乎都会写成:
python
# 危险写法
for group in groups:
send_text(group.guid, group.id, content) # 一秒内打满
这段代码功能上对,但它是触发风控最典型的行为:短时间、高频率、节奏机械。跑一两次可能没事,量大、跑得久,账号一定出问题。
正确的做法是节流------每条之间留随机间隔,模拟真人节奏:
python
import time, random
def safe_batch_send(tasks):
for t in tasks:
send_text(t.guid, t.to_id, t.content)
time.sleep(random.uniform(8, 25)) # 随机间隔,避免机械节奏
几个细节决定成败:
- 间隔必须随机,固定 10 秒比随机 8~25 秒更容易被识别
- 避开异常时段,深夜不做高频群发
- 单账号每天设上限,别一个号一天发几千条
- 新账号要预热,刚登录的号从低频开始慢慢加
三、第二个坑:群分散在多个节点,且要并发
真实场景里,几百个外部群不会都挂在一个账号下,而是分散在多个登录节点。这带来两个变化:
- 每条任务都得知道自己属于哪个节点(guid 不同)
- 节流要按节点分别算------风控是针对单账号的,节点 A 发得慢不代表 B 也得跟着慢
所以正确的并发模型是:节点之间并行,单节点内部串行 + 节流 。用 asyncio 实现:
python
import asyncio, random
import httpx
from collections import defaultdict
GATEWAY = "http://api.qiwx.online/work-weixin/api/doApi"
TOKEN = "your_token"
async def send_text(client, guid, to_id, content):
r = await client.post(
GATEWAY,
headers={"Content-Type": "application/json", "X-QIWEI-TOKEN": TOKEN},
json={"method": "/msg/sendText",
"params": {"guid": guid, "toId": to_id, "content": content}},
timeout=30,
)
return r.json()
async def node_worker(client, guid, tasks):
"""单个节点:串行 + 节流"""
for t in tasks:
await send_text(client, guid, t["to_id"], t["content"])
await asyncio.sleep(random.uniform(8, 25))
async def run(all_tasks):
# 按节点分组
by_node = defaultdict(list)
for t in all_tasks:
by_node[t["guid"]].append(t)
async with httpx.AsyncClient() as client:
# 每个节点一个 worker,节点之间并发
await asyncio.gather(*[
node_worker(client, guid, tasks)
for guid, tasks in by_node.items()
])
这套结构的妙处:整体吞吐 = 节点数 × 单节点速率。10 个节点并行,就能在保证每个账号安全节奏的前提下,把总发送量提升 10 倍。这才是"高效"的正确实现方式------不是把单账号催快,而是靠多节点并发跑总量。
四、第三个坑:节点掉线,消息进黑洞
最隐蔽的故障:任务发出去了,HTTP 也返回了,但那个节点其实早掉线了,消息根本没到。
解决办法是发送前先确认节点在线:
python
async def node_worker(client, guid, tasks):
for t in tasks:
if not await is_online(client, guid): # 发之前先查在线
await requeue(t) # 掉线则重排/换节点
continue
await send_text(client, guid, t["to_id"], t["content"])
await asyncio.sleep(random.uniform(8, 25))
配套做法:
- 订阅登录状态事件,节点上下线实时感知
- 定期主动巡检,不只依赖事件
- 掉线告警,发不出去要有人知道
- 失败重试,但重试也走节流,不能瞬间堆积
主动发送的可靠性,本质上等于节点在线率。
五、第四个坑:内容个性化与幂等
批量发往往不是发同一句话,而是带名字、带订单号、带专属信息。这要求任务支持模板:
python
content = template.format(name=group["owner"], order_no=order["no"])
更重要的是幂等。脚本重跑、任务重试、手抖点两次,都可能让客户收到重复消息。给每条任务一个唯一 key,发送前先查是否发过:
python
async def node_worker(client, guid, tasks):
for t in tasks:
if await already_sent(t["dedup_key"]): # 发过就跳过
continue
await send_text(client, guid, t["to_id"], t["content"])
await mark_sent(t["dedup_key"])
await asyncio.sleep(random.uniform(8, 25))
对外部客户来说,重复推送的体验伤害比晚一点收到大得多。
六、完整的工程版骨架
把上面四个坑都迈过去,一个能放生产的 Python 主动发送脚本大致是这样:
python
async def production_send(all_tasks):
by_node = defaultdict(list)
for t in all_tasks:
by_node[t["guid"]].append(t)
async with httpx.AsyncClient() as client:
async def worker(guid, tasks):
for t in tasks:
if await already_sent(t["dedup_key"]):
continue
if not await is_online(client, guid):
await requeue(t); continue
if not in_active_hours():
await wait_until_active_hours()
try:
await send_text(client, guid, t["to_id"], t["content"])
await mark_sent(t["dedup_key"])
except Exception:
await requeue(t) # 失败重排
await asyncio.sleep(random.uniform(8, 25))
await asyncio.gather(*[worker(g, ts) for g, ts in by_node.items()])
设计原则总结成四条:
- 一切发送走节流,绝不裸循环
- 按节点拆并发,单账号安全 + 多节点高效
- 发前校验在线,掉线重排不静默失败
- 模板 + 幂等,个性化且不重复
七、写在最后:能发 ≠ 该发
Python 实现主动发送,技术上不难------httpx + asyncio + 节流队列,这套组合很成熟。真正区分专业与否的,是在"能发"和"该发"之间守住分寸:
- 同一个群一天发几条要有上限
- 非工作时间不主动打扰
- 推的是客户真正关心的内容(订单、提醒),不是纯广告
- 给客户留退订的途径
把发送能力做强,同时把发送节奏做克制,外部群主动消息发送才能既高效,又不伤客户关系。这也是为什么生产级的主动推送,从来不是"写个 for 循环调接口"那么简单。
如果你要落地,建议先用 Python 跑通一条最小链路:一条带 guid 和 dedup_key 的任务 → 校验节点在线 → 节流发出 → 确认到达。这条跑顺了,多节点并发、模板个性化、失败重试都是在它之上叠加的事。先把"稳"做出来,再追"快"。