☰
用Python与pywinauto实现PC微信批量自动发消息实战
2026/10/5 6:05:12 网站建设 项目流程

1. 这个脚本的由来:重复操作做到想吐之后的产物

1.1 每天重复半小时的机械劳动

最开始想做这个项目,纯粹是被“复制粘贴”逼的。我当时负责对接一批合作方,每天要挨个在PC微信里搜索联系人、输入相同内容、点击发送,一聊就是二三十个窗口。第一天勉强能忍,第二天手就开始发酸,到了第三天我发现自己在盯着某个对话框发呆——因为内容太多太像,我已经分不清刚才这条消息到底发了没发。

那段时间网上也有一些“微信多开助手”“群发工具”,但要么用起来要装一堆全家桶,要么就是把消息模板硬编码在软件里面,改一句话都要重启程序。更关键的是,这类工具普遍闭源,我根本不知道它到底是怎么操作微信的,也没有办法验证安全性。与其把微信账号和数据交给来历不明的工具,不如自己写一个脚本。

于是就有了这个项目:用Python加pywinauto,直接操作PC微信的窗口控件,实现指定联系人、指定内容,一条龙自动发送。核心目标是稳定、可控、能看懂每一行代码在干什么。

1.2 pywinauto在这场选型里赢在哪

市面上能操作Windows桌面程序的方案不少,我在动手之前也做了简单对比。现在回头看这个选型过程,其实可以给后来人省不少时间。

方案原理优点缺点
pywinauto通过Windows底层UI消息模拟点击、键盘输入成熟稳定,对窗口、控件状态感知能力强,代码开源可审计对DirectUI这类自绘控件需要额外处理
PyAutoGUI基于屏幕坐标和截图找图简单粗暴,上手快分辨率一变就崩,窗口位置稍微移动就点错
按键精灵录制和回放鼠标键盘动作不需要写代码脚本逻辑复杂一点就非常难维护,且对控件状态完全无感知
uiautomation基于微软UI Automation框架对现代应用适配好目录结构相对复杂,部分自绘控件同样抓不到
Selenium面向浏览器生态好,资料多根本没有PC微信的方案,换个思路反而麻烦

pywinauto在windows平台上算是最“接地气”的选择:它支持win32和uia两种backend,既能识别传统Win32控件,又能处理一部分现代UI元素。更重要的是,pywinauto允许我直接拿到窗口的句柄和控件树状态,而不是像PyAutoGUI那样只能靠坐标“盲人摸象”。

而且pywinauto本身是纯Python库,license友好,依赖少,安装时不用带一堆乱七八糟的二进制文件。对于我这种只想要一个脚本、不想陷入IDE工程化的人来说,它就是最合适的。

所以整个项目的技术栈最后确定为:

  • 语言:Python 3.8+(兼容3.11)
  • GUI自动化库:pywinauto
  • 额外的系统能力:pywin32(用来做剪贴板操作和窗口置顶判断)
  • 运行环境:Windows 10 / Windows 11
  • 目标应用:PC微信 3.9.x版本(不同版本控件结构略有差异,后面细说)

如果你也面临类似的重复发消息需求,先别急着找那种“一键群发”的外部工具。这个脚本的每一行你都能看懂,出了问题你知道去哪里修,这种掌控感是用第三方工具买不来的。

2. 环境搭建与pywinauto原理:先理解它怎么工作,再动手

2.1 环境准备里最容易翻车的三个环节

官方文档把安装写得像一句话的事:pip install pywinauto。但真到了项目里,我至少见过三次翻车现场。

第一次是Python版本和pip的对应关系搞错了。很多人电脑上装了不止一个Python版本,命令行里直接敲pip install可能会装到旧版本解释器对应的site-packages目录里。后来我统一使用python -m pip install pywinauto这种方式,确保装到当前python解释器所在的环境里。

第二次是权限问题。如果你是在公司电脑的操作系统上运行,可能会出现Access Denied之类的安装失败提示。这种情况要么给当前用户目录下的site-packages写权限,要么就老老实实加--user参数。个人推荐直接加:

python -m pip install pywinauto --user

这样不依赖系统级管理员权限,也避免污染全局环境。

第三次是缺少运行时依赖。pywinauto和pywin32配合最紧密,执行某些剪贴板和窗口相关操作时会需要pywin32里面的API。建议把依赖一并装上:

python -m pip install pywinauto pywin32

装完以后,打开Python命令行验证一下:

from pywinauto import Application print(Application)

能正常打印出模块信息,说明环境OK。这一步别省,很多人直接在IDE里跑脚本,报的却是环境问题,排查起来特别浪费时间。

2.2 pywinauto两种backend:win32和uia,到底选哪个

pywinauto有两个核心backend,很多人一上来就懵。简单说:

  • win32 backend:基于Windows的传统消息机制,适合老式Win32应用、MFC程序、标准控件。
  • uia backend:基于UI Automation框架,适合现代应用、UWP应用、部分自绘程度没那么深的第三方程序。

微信PC版的界面虽然是自绘的,但主窗口的很多基础交互仍然保留了Win32消息特性。我更推荐对微信使用win32 backend,原因有几个:

第一,win32 backend获取窗口句柄和顶层窗口的效率更高,响应速度更快,脚本执行起来更接近真人操作节奏。

第二,win32 backend在处理“发送消息”时支持SendMessage和PostMessage这类底层消息,可以绕过一部分鼠标物理坐标问题。

第三,uia backend在微信3.9+版本上抓到的控件树非常奇怪,很多区域显示成无名的Pane,反而不如win32 backend干净。

当然,win32 backend也有自己的脾气:它识别不了所有自绘控件内部的“假按钮”。但我们的策略本来也不依赖内部控件遍历,而是直接操纵最外层的窗口区域,搭配键盘快捷键。这个思路在后面源码部分会详细讲。

3. 微信窗口的DirectUI问题:为什么标准控件定位会失灵

3.1 用inspect工具看微信界面,你会怀疑人生

如果你以前用FindWindow、EnumChildWindows这类API扫过其他软件,会得出一个直觉:顶层窗口下面应该有一层一层的子窗口,子窗口里面有按钮、编辑框、文本框,一层一层都能查到。

但微信不一样。PC微信大量使用DirectUI技术,也就是由父窗口自己绘制内部元素,不创建标准子窗口。你用微软官方工具inspect.exe去看微信的聊天区域,会发现里面几乎是一片空白——没有Edit控件,没有Button控件,缓冲区和编辑框的句柄要么不暴露,要么就是同一个大句柄。

这意味着什么?意味着我们没法用:

app.window(class_name='Edit').set_text('hello')

这种教科书式的方式去写入消息。你要是真按传统思路去遍历控件树,折腾一晚上都不一定能找到输入框。

3.2 不硬碰控件,改用窗口定位 + 键盘操作

我的应对思路是“外层定位全靠窗口,内层交互全靠键盘”。

具体拆成三步:

  1. 用pywinauto定位微信主窗口,确认窗口存在、前台激活。
  2. 用“快捷键+搜索”方式找到目标联系人,而不是尝试点击联系人列表里的每一项。
  3. 消息输入框,用微信自己的快捷键(比如Ctrl+F定位到搜索框、Enter键进入会话、Tab键或鼠标点击进入消息输入区、Ctrl+Enter或Enter发送消息)。

这套方案的优点在于:即使微信内部是DirectUI,它也必须响应Windows层面的键盘消息和窗口激活消息。我们只要保证窗口处于正确状态,然后模拟用户按键行为,就能绕过控件识别难题。

我把这套策略称为“人怎么操作,代码就怎么操作”。实际效果最稳定,后期维护成本也最低。你可能担心这样是不是不够“专业”,但做GUI自动化最重要的是跑得稳,不是为了代码看起来好看。

4. 完整源码拆解:从窗口查找到消息发出

4.1 主流程设计

先看整体流程,我用文字描述一下:

  1. 启动微信(如果没开的话),等待主窗口出现。
  2. 激活主窗口,确保窗口在前台。
  3. 搜索联系人:在微信主窗口顶部搜索框输入联系人备注名。
  4. 等待搜索结果出现,按Enter进入聊天窗口。
  5. 确认消息输入区域获得焦点,粘贴或输入预置内容。
  6. 按发送快捷键。
  7. 关闭当前聊天窗口或直接搜索下一个联系人(用Ctrl+F回到搜索框)。

下面是我实际在用的完整脚本,注释尽量详细:

import time import datetime import pywinauto from pywinauto import Application, keyboard from pywinauto.findwindows import ElementNotFoundError # 微信主窗口标题前几个关键字,不同版本可能有差异 WECHAT_TITLE_KEYWORD = "微信" def wait_for_window(title_keyword, timeout=20, interval=0.5): """ 等待微信主窗口出现 用pywinauto的Desktop对象轮询窗口列表 """ deadline = time.time() + timeout while time.time() < deadline: try: desktop = pywinauto.Desktop(backend="win32") windows = desktop.windows() for w in windows: if title_keyword in (w.window_text() or ""): return w except Exception as e: print("扫描窗口异常:", e) time.sleep(interval) raise ElementNotFoundError(f"在{timeout}秒内没有找到标题包含“{title_keyword}”的窗口") def activate_wechat(win): """ 激活窗口并置前 set_focus在某些自绘窗口上不稳定,所以用restore + set_focus组合 """ try: win.restore() win.set_focus() time.sleep(0.5) except Exception: # 某些最小化状态下restore会抛异常,做一次兜底 win.minimize() win.restore() time.sleep(0.3) win.set_focus() time.sleep(0.5) def search_contact(win, name): """ 通过搜索框定位联系人 这里使用Ctrl+F唤出搜索框,模拟输入后按回车 """ win.set_focus() keyboard.send_keys("^f") # Ctrl+F time.sleep(0.5) keyboard.send_keys(name, with_spaces=True) time.sleep(1.0) # 搜索结果的第一个通常是联系人 keyboard.send_keys("{ENTER}") time.sleep(0.8) def send_message(win, message, send_key="enter"): """ 向当前聊天窗口发送消息 先确保输入框聚焦,再粘贴文本,最后发送 """ win.set_focus() time.sleep(0.3) # 把文本放入剪贴板再粘贴,避免特殊字符被键盘解析错 write_to_clipboard(message) keyboard.send_keys("^v") # Ctrl+V 粘贴 time.sleep(0.5) if send_key == "ctrl_enter": keyboard.send_keys("^{ENTER}") # 微信里Ctrl+Enter换行,部分版本发送快捷键是Enter else: keyboard.send_keys("{ENTER}") # 默认回车直接发送 time.sleep(0.5) def write_to_clipboard(text): """ 用pywin32写剪贴板 这个函数比keyboard.type_keys更稳,因为不涉及字符串里的特殊字符转义 """ import win32clipboard win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text, win32clipboard.CF_UNICODETEXT) win32clipboard.CloseClipboard() def ensure_wechat_running_ready(): """ 确保微信运行,主窗口显示并定位 """ try: app = Application(backend="win32").connect(title_re=".*微信.*") except Exception: # 没有找到微信窗口,尝试启动 import subprocess subprocess.Popen(r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe") retry = 0 while retry < 30: try: app = Application(backend="win32").connect(title_re=".*微信.*") break except Exception: retry += 1 time.sleep(0.5) if app is None: raise Exception("微信启动失败,请确认安装路径或手动打开微信") main_win = app.window(title_re=".*微信.*") activate_wechat(main_win) return main_win def main_batch_send(contact_list, message): main_win = ensure_wechat_running_ready() for name in contact_list: try: print(f"正在处理: {name}") search_contact(main_win, name) send_message(main_win, message, send_key="enter") time.sleep(2) # 每次间隔2秒,给微信处理时间 except Exception as e: print(f"发送给 {name} 时失败: {e}") # 记录错误日志 with open("send_error.log", "a", encoding="utf-8") as f: f.write(f"{datetime.datetime.now()} 联系人: {name} 错误: {e}\n") if __name__ == "__main__": contacts = ["张三", "李四", "王五"] msg = "您好,这是今天的例行同步消息,请查收。" main_batch_send(contacts, msg)

4.2 搜索联系人与选中会话的细节

很多人对search_contact这个函数有疑问:为什么搜索后不检查结果?为什么不判断“找不到联系人”的情况?

实际上我在早期版本里加过对搜索结果的控件判断,但微信3.8/3.9版本搜索结果的控件树不可靠,反而会因为找不到控件误报失败。后来我把判断简化成“按Enter后,等待0.5到1秒,检查当前窗口是否还在主窗口”。如果还在主窗口,就说明搜索没有匹配到联系人,这时候需要处理。

下面是升级版的search_contact:

def search_contact(win, name): win.set_focus() keyboard.send_keys("^f") time.sleep(0.3) keyboard.send_keys(name, with_spaces=True) time.sleep(1.0) keyboard.send_keys("{ENTER}") time.sleep(0.8) # 判断是否进入了聊天窗口:当前窗口rect是否变化,或者标题是否变成联系人备注名 # 如果title_change_expected和实际一致,说明成功

你会看到我用了一个“标题变化”作为进入聊天窗口的判断依据。微信主窗口的标题一般是“微信”,进入联系人聊天后,标题会变成联系人备注名。利用这一点,不需要解析控件,直接读title就行。

4.3 消息输入:为什么不直接用set_edit_text,而是用剪贴板

之前提过微信的输入框不是标准Edit,所以set_edit_text大概率无效。我最开始用过keyboard.type_keys直接敲键盘打字,但当消息内容里有括号、引号、百分号这些特殊字符时,偶尔会被pywinauto的语法解析搞乱:

# 这种写法会有问题 keyboard.type_keys("价格是(100)元,折扣为50%")

括号在type_keys里本身就带特殊含义,会造成转义错误。虽然可以用type_with_tap或重复花括号转义,但只要消息模板一复杂,就总有意外。

最终可靠方案就是先把文本放到系统剪贴板,然后模拟Ctrl+V。这个方案完全不经过键盘缓冲区,内容是什么样、粘贴出来就是什么样。注意在write_to_clipboard里用了CF_UNICODETEXT,这样中文、Emoji都不会乱码。

4.4 批量发送和内容模板的扩展

如果你的需求是每个联系人发同一份固定内容,上面的main_batch_send就够用了。但实际项目中,“每个联系人都要发一模一样的内容”的情况其实非常少。更多时候是内容里需要带上联系人名字,或者根据不同类型发不同模板。

我习惯用占位符替换的方式处理:

def build_message(template, name, extra=""): message = template.replace("{{name}}", name) message = message.replace("{{extra}}", extra) return message

然后主循环里这样调:

template = "{{name}}您好,本周的会议时间定在周三下午3点。{{extra}}" for name in contacts: msg = build_message(template, name, "如果有问题请提前告知,谢谢。") search_contact(main_win, name) send_message(main_win, msg)

用占位符的好处是,以后模板内容变更时只改一个地方,不需要改整体代码。如果你还要按Excel表格里的名单批量发送,那只需要把contact_list改成从pandas读取即可,剩下的流程完全复用。

5. 实测中踩过的坑,以及针对性优化

5.1 输入法乱入导致内容错乱

这是我遇到的第一大坑。脚本运行的时候,如果你的系统默认输入法是搜狗输入法或者微软拼音的中文模式,粘贴消息后按Enter发送时,输入法可能会把回车键当作“候选词确认”,导致消息没有发出去,或者发出去了半截内容。

解决办法很简单:发送前临时切换到英文输入法模式。可以用pywinauto直接往前台窗口发送Shift键切换中英文。更省事的办法是,在send_message函数里加一个“清空输入法状态”的步骤:

keyboard.send_keys("{SHIFT}") # 切换中英文输入法 time.sleep(0.2)

如果输入法还是状态不稳定,更硬核的方案是在Windows里把默认输入法设置为纯英文,或者用第三方工具锁定输入法状态。实际使用下来,Shift切换已经解决了95%的问题。

5.2 聊天窗口被遮挡或失焦后怎么自愈

脚本跑着跑着,如果你手动碰一下鼠标,或者弹出一个系统通知把微信窗口顶下去了,那么send_message里的set_focus可能会失败,或者焦点被其他窗口抢走。这会导致消息误发到其他程序里,风险很大。

我在代码里加了“前置窗口校验”:

import win32gui import win32process def is_wechat_foreground(win): hwnd = win32gui.GetForegroundWindow() return hwnd == win.handle

每次send_message之前,先确认当前前台窗口确实是微信主窗口,否则就重新activate_wechat,再等待0.5秒。这个校验非常重要,能避免你正在写文档的时候脚本突然跳出来把字打到别的地方。

5.3 高频批量发送的“冷静期”设计

连续给几十个人发消息,如果脚本跑得像机关枪一样,中间没有任何间隔,微信可能会弹出安全验证提示,虽然不至于封号,但会打断流程。我的经验是每条消息发送后至少sleep 1.5到2秒。

如果你要发的数量特别大,建议把间隔拉长到4到5秒。还可以随机引入0.5到1.5秒的抖动:

import random time.sleep(1.5 + random.uniform(0, 1.5))

这样做的目的是让操作节奏更接近真人。别觉得这个多余,批量自动发消息最忌讳的就是节奏太机械。

5.4 错误恢复与日志记录

脚本执行中不可避免会出现意外:网络断开、联系人不存在、微信窗口闪退等。我在主循环里加了try/except,并把失败记录写入日志文件,这样即使中途出错,后面也能根据日志找到是哪一个联系人发送失败,手动补发即可。

def main_batch_send(contact_list, message): main_win = ensure_wechat_running_ready() success_list = [] failed_list = [] for name in contact_list: try: search_contact(main_win, name) send_message(main_win, message, send_key="enter") success_list.append(name) print(f"[OK] {name}") except Exception as e: failed_list.append(name) print(f"[FAIL] {name}: {e}") time.sleep(1.5 + random.uniform(0, 1.5)) print("成功:", success_list) print("失败:", failed_list) with open("send_report.txt", "w", encoding="utf-8") as f: f.write("成功名单:\n") f.write("\n".join(success_list)) f.write("\n失败名单:\n") f.write("\n".join(failed_list))

有了这份报告,你后续处理起来就非常精准。别小看这个细节,真正跑大批量的时候,它能帮你少熬不少夜。

6. 使用边界与几个重要的提醒

6.1 这个脚本能做什么,不能做什么

这套脚本适合的场景是:日常例行通知、工作沟通、给固定联系人发送同类型模板消息。它很直接,它最多能做到“替代你一个人手动来回操作”。

但需要明确说清边界:

  • 它不能替代微信官方客服系统,也不适合成为营销群发工具。
  • 它只操作你本机已登录的微信账号,不会有“绕过验证”“越权”之类的能力。
  • 如果微信改版导致窗口标题、界面结构、快捷键变化,脚本可能需要对应调整。
  • 高频、大批量发送会触发微信安全策略,这不是脚本能绕开的。

6.2 如何降低风险系数

我自己实际跑批量任务时有一条原则:单次发送人数控制在50以内,单条间隔不低于2秒,全天累计发送量不追求极端。如果真有上千人的触达需求,正规的做法是使用企业微信的客户群发功能或服务号模板消息,而不是用GUI脚本硬撑。

脚本只能做锦上添花,别拿它去挑战风控体系,这是我对所有想“玩大的”朋友的一句真心话。

6.3 后续可以扩展的方向

这套脚本的思路不限于发微信。pywinauto能操作的桌面软件很多,比如自动填Excel表格、自动操作企业OA系统、自动在钉钉/QQ里回复消息等。原理都是一样:定位窗口,模拟键盘鼠标操作。

你甚至可以把脚本封装成定时任务,每天上午9点自动给团队成员发日报提醒。配合Windows的计划任务程序,做到无人值守也很简单。

我在实际运营中发现,把多个小自动化脚本组合起来,真的能省出不少时间。最明显的例子就是,以前每天到公司先花二十分钟手动发消息,现在脚本自动跑,我只需要看一眼执行日志确认有没有失败记录就行。

如果你只是想解决“重复发消息”这一个痛点,照着我上面这份代码改一改联系人名单和消息模板,基本就能跑了。等你跑顺了,再回头看,你会发现手动复制粘贴这个动作,早就不该占用你每天那么多时间了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询