ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python+pywinauto实现PC微信自动发消息:从控件探针到完整源码

用Python+pywinauto实现PC微信自动发消息:从控件探针到完整源码 最近接了一个挺枯燥的需求每天早上要把同一份工作模板消息分别发给几个不同的同事。手动复制粘贴一次两次还能忍天天这么干真的浪费时间。于是我用 Python 脚本 pywinauto 写了一个 PC 微信消息自动发送的小工具把“找人、粘贴、回车”这几步彻底交给代码。整套源码我会贴在后面直接复制就能跑。这套方案能覆盖的场景还真不少运维值班时给值班群发报警摘要、接口测试结束后把结果自动发给相关人、给自己多个微信号互相对账、做 RPA 流程里的最后一步通知全都能用上。适合两类人一是想学 Windows GUI 自动化的开发者二是被重复消息困扰、想用脚本解放双手的办公效率爱好者。在动手之前有句丑话说在前面微信官方对任何自动化操作都有管控这套脚本只适合个人合法、低频、非营销的场景比如个人提醒和测试通知。如果你拿它去做批量群发、骚扰消息、营销引流轻则弹出安全验证重则限制登录后果自己负责。1. 动手前的思路为什么偏偏用pywinauto1.1 自动发微信的几条路线对比做这件事之前我其实在论坛里翻了不少方案总结下来主要有三条路线逆向协议模拟、第三方Hook库、以及 GUI 自动化。各自优缺点非常明显我直接列个表对比方案原理优点缺点适合谁逆向协议模拟抓取客户端与服务器的交互数据模拟发送请求效率高、可以批量、不依赖界面加密算法随时变、封号风险极高、维护成本大专业安全方向不建议普通开发者碰第三方Hook库/Wechaty注入或中间件方式调用微信能力接口封装好、功能丰富微信官方多次打击稳定性看运气不少要付费有一定开发经验、能接受风险的人pywinauto GUI自动化模拟鼠标键盘像人一样操作界面不碰协议、门槛低、代码逻辑直观、易维护速度慢、依赖窗口结构、不适合超大规模个人自动化、办公提效、测试场景我的需求是每天固定发几条消息量不大但对稳定性有要求。逆向协议首先排除因为我不想去研究加密算法更不想冒封号风险。第三方Hook库我也试过前期还挺好用后来微信一更新就不行了维护起来心累。最后选了 pywinauto本质上就是让脚本当个“人”找到微信窗口、找到输入框、输入内容、按回车一切操作和手动一摸一样。微信那边看到的就是普通用户操作相对安全代码逻辑也好理解。1.2 pywinauto的底层原理一句话说清楚pywinauto 是 Windows 平台下的 GUI 自动化库底层依赖两套东西Win32 API 和 UI Automation。Win32 API 是老牌桌面应用接口UI Automation 是微软后来推出的可访问性框架专门用来给屏幕阅读器、自动化测试工具提供界面元素信息。pywinauto 把这两套东西封装成了一套统一的控件操作接口开发者不需要直接和 C 打交道。打个比方屏幕上的窗口就像一栋房子窗口里的按钮、输入框、列表就是房间里的家具。pywinauto 做的事情是先找到房子窗口再找到你要的家具控件最后对家具执行操作点击、输入文字。整个流程就是“找窗口 → 找控件 → 操作控件”三步走。这里有个重要的参数叫 backend取值一般是 uia 或 win32。win32 backend 走的是 Win32 API响应快但很多新界面框架里的元素抓不到uia backend 走 UI Automation对现代 Windows 应用支持更好。PC 微信这种界面结构复杂、大量自绘控件的软件实测下来 uia 的探测能力明显更强所以代码里我全部用的 backenduia。判断标准很简单探针脚本打印出来的控件信息足够多就用 uia这是最靠谱的选择。2. 环境准备与控件探针2.1 安装Python、pywinauto和pywin32整个环境搭建分三步都是在 Windows 命令行里搞定。第一步确认 Python 环境。我用的是 Python 3.10理论上 Python 3.7 以上都能跑。没装 Python 的话去官网下载安装包安装时记得勾选“Add Python to PATH”不然命令行找不到 python 命令。第二步安装依赖库。执行下面这条命令pip install pywinauto pywin32 schedulepywinauto 是自动化的核心pywin32 提供 win32clipboard 和 win32con我们用它来操作剪贴板schedule 是做定时任务用的如果暂时用不到也可以不装。装完可以验证一下版本python -c import pywinauto; print(pywinauto.__version__)第三步把 PC 微信登录好保持主窗口在桌面上。这一步很关键脚本连接的是“已登录的微信主窗口”如果微信没登录窗口都找不到后面什么都做不了。2.2 第一段探针代码看看微信控件长什么样写自动化脚本最忌讳上来直接写业务逻辑因为你根本不知道微信窗口里的搜索框、输入框在 pywinauto 眼里长什么样。所以第一步是用探针脚本把微信界面的控件树打印出来。新建一个probe.py内容如下from pywinauto import Desktop desktop Desktop(backenduia) win desktop.window(title_re.*微信.*) if win.exists(timeout5): win.set_focus() win.print_control_identifiers() else: print(没有找到微信窗口请确认已登录且主窗口处于打开状态)运行之后终端里会输出一长串控件树结构类似这样Dialog - 微信 (L526, T218, R1394, B937) [微信Dialog] | Pane - 会话 (L...) | | Edit - 搜索 (L...) | | [搜索Edit, Edit1] | Pane - 聊天 (L...) | | Edit - 输入 (L...) | | [输入Edit, Edit2]不同版本的微信输出格式会有差异但大方向一致顶部有搜索框底部聊天区域有消息输入框。你只需要找到两个关键点一个是标题或 name 里带“搜索”的 Edit 控件用来搜联系人另一个是聊天输入框用来写消息。这些信息记下来后面源码里直接用。注意一个细节如果你的 Windows 桌面开启了缩放比如 125%、150%控件坐标会偏移但 pywinauto 定位控件走的是逻辑坐标一般不受影响。真遇到了可以在代码开头设置进程 DPI 感知import ctypes ctypes.windll.shcore.SetProcessDpiAwareness(1)2.3 微信窗口识别的几个小坑探针代码里我用了title_re.*微信.*这其实是踩过坑之后的选择。微信窗口的标题不是固定的没打开聊天时叫“微信”打开某个联系人后标题会变成“微信 张三”有时还会带上“的聊天”之类的后缀。如果你用精确匹配窗口一换标题就找不到了用正则匹配“微信”两个字不管标题怎么变都能命中。另外微信窗口有自己固定的类名老版本叫WeChatMainWndForPC新版微信我用的时候看到的是WeChatMainWinForPC之类的名字。可以在探针脚本里加一句print(win.class_name())打印出来然后在连接时加上class_name_re做双重校验防止匹配到其他带“微信”字样的窗口。如果电脑上开了多个微信窗口desktop.window()会匹配到第一个建议先遍历所有窗口选择你真正想操作的那个。多开场景下最好在函数里加一个窗口标题或 PID 的参数不要偷懒写死不然发错联系人真的会出事。3. 核心源码一套能直接跑的自动发送脚本3.1 完整源码环境有了控件信息也看到了下面直接上完整源码。这份代码支持三种调用方式命令行指定联系人和消息、连续发送多条、通过 CSV 文件批量发送。注释写在代码里复制到本地就能跑。import time import argparse import csv from typing import Optional from pywinauto import Desktop from pywinauto.keyboard import send_keys import win32clipboard import win32con # 延时配置给微信界面留出响应时间单位秒 SCAN_INTERVAL 0.8 INPUT_DELAY 0.3 POST_SEND_DELAY 0.5 def copy_to_clipboard(text: str): 把文本写入系统剪贴板后续用 CtrlV 粘贴到微信输入框。 win32clipboard.OpenClipboard() try: win32clipboard.EmptyClipboard() win32clipboard.SetClipboardData(win32con.CF_UNICODETEXT, text) finally: win32clipboard.CloseClipboard() def connect_wechat(): 连接已登录的PC微信主窗口找不到则返回 None。 desktop Desktop(backenduia) candidates [ {title_re: .*微信.*, class_name_re: WeChatMain.*}, {title_re: .*微信.*}, ] for params in candidates: try: win desktop.window(**params) if win.exists(timeout2): win.set_focus() return win except Exception: continue return None def open_contact(win, contact: str): 通过顶部搜索框打开指定联系人会话。 search_edit None try: search_edit win.child_window(title_re搜索, control_typeEdit) if not search_edit.exists(timeout1): search_edit None except Exception: pass if search_edit is None: # 兜底方案用 CtrlF 唤起搜索框 send_keys(^f) time.sleep(0.5) search_edit win.child_window(control_typeEdit, found_index0) search_edit.click_input() time.sleep(INPUT_DELAY) search_edit.set_edit_text(contact) time.sleep(SCAN_INTERVAL) send_keys({ENTER}, pause0.05) time.sleep(SCAN_INTERVAL) def send_text(win, msg: str): 把消息写入聊天输入框并回车发送。 copy_to_clipboard(msg) edit None try: edit win.child_window(title_re输入|消息, control_typeEdit) if not edit.exists(timeout1): edit None except Exception: pass if edit is None: # 兜底方案点击聊天区域底部的输入框大概位置 rect win.rectangle() click_x rect.left rect.width() // 2 click_y rect.bottom - 60 win.click_input(coords(click_x, click_y)) time.sleep(INPUT_DELAY) edit win.child_window(control_typeEdit, found_index0) edit.click_input() time.sleep(INPUT_DELAY) send_keys(^v) time.sleep(INPUT_DELAY) send_keys({ENTER}) time.sleep(POST_SEND_DELAY) def send_to_contact(contact: str, msg: str, count: int 1): 向单个联系人连续发送 count 条消息。 win connect_wechat() if win is None: raise RuntimeError(未找到PC微信主窗口请确认微信已登录且处于常规界面) open_contact(win, contact) for _ in range(count): send_text(win, msg) def send_from_csv(csv_path: str, msg: str): 从CSV文件读取联系人列表并逐个发送默认只处理前5个。 with open(csv_path, newline, encodingutf-8) as f: reader csv.reader(f) rows [row for row in reader if row and row[0].strip()] # 安全限制超过5个联系人就不自动处理 rows rows[:5] for row in rows: contact row[0].strip() print(f发送给{contact}) send_to_contact(contact, msg) time.sleep(1.0) if __name__ __main__: parser argparse.ArgumentParser(descriptionPC微信消息自动发送个人合法场景) parser.add_argument(--contact, -c, help联系人名称) parser.add_argument(--msg, -m, help要发送的消息内容) parser.add_argument(--count, -n, typeint, default1, help发送次数默认1次) parser.add_argument(--csv, help联系人CSV文件路径文件第一列为联系人名称) args parser.parse_args() if args.csv: send_from_csv(args.csv, args.msg) elif args.contact and args.msg: send_to_contact(args.contact, args.msg, args.count) else: parser.print_help()运行示例python send_wechat.py --contact 张三 --msg 明天下午三点开会记得带电脑 python send_wechat.py --contact 张三 --msg 提醒 --count 3 python send_wechat.py --csv contacts.csv --msg 本周周报已提交请查收3.2 逐段拆解这几行是真正的关键代码不长但有几个细节值得重点说一下都是实际跑过之后才明白的。第一个细节是剪贴板方案。很多新手喜欢用 edit.type_keys(msg) 直接输入文字但这个函数对中文支持极差经常出现输入一堆乱码或者字符丢失的情况。更稳的方式是先把文本写进系统剪贴板然后向输入框发送 CtrlV最后按回车发送。copy_to_clipboard函数用到了 win32clipboard注意操作完一定要CloseClipboard()不然其他程序会拿不到剪贴板内容。我在try/finally里做了保护防止中途异常导致剪贴板锁死。第二个细节是窗口连接的兜底逻辑。微信老版本和新版本的窗口类名不一样新版我这边实测WeChatMain.*能匹配到但谁也不能保证以后微信不换类名所以connect_wechat里准备了两组匹配条件。第一组带类名校验更精准第二组只用标题正则覆盖更广。这两组按顺序尝试保证了兼容性。第三个细节是控件找不到时的兜底坐标点击。微信界面有时候会因为版本差异导致child_window找不到输入框。这种情况我直接计算微信窗口内部的相对位置点一下聊天区域底部大概率能点中输入框。这个做法看着粗暴但在自动化脚本里非常实用。你可以在探针脚本里跑一次拿到自己电脑上输入框的实际坐标然后把click_y调整成适合你分辨率的数值。第四个细节是延时控制。每次操作之间我都加了time.sleep不是瞎加而是有讲究的。微信界面切换、搜索联系人、打开会话都需要时间延时太短脚本容易卡在“输入框还没准备好”的状态延时太长整个发送流程太慢。实测SCAN_INTERVAL设在 0.8 秒左右比较平衡既能稳定跑又不会慢到让人抓狂。3.3 定时发送与批量发送的接法如果你想把脚本变成真正的“无人值守”可以用 schedule 库做定时任务。下面是示例代码import schedule import time # 每天早上9点发日报提醒 schedule.every().day.at(09:00).do( send_to_contact, contact张三, msg早上好记得提交昨日日报 ) # 每小时整点发一次例行提醒 schedule.every().hour.at(:00).do( send_to_contact, contact李四, msg例行巡检提醒 ) while True: schedule.run_pending() time.sleep(1)批量发送的场景我建议用 CSV 文件方便维护联系人列表。文件格式很简单第一列是联系人名称后面可以放备注脚本只取第一列。注意send_from_csv里我加了一行rows rows[:5]这是有意为之的安全限制。批量发送在这个脚本里只适合你真正需要通知的那几个联系人而不是让你拿去做群发工具。数量一旦扩大微信的风控机制很容易盯上你而且对接收方来说也是骚扰。如果你确实有几百个校验过的老客户需要发通知正确做法是走企业微信的客户联系 API或者用微信官方认可的公众号模板消息而不是脚本模拟点击。这一点一定要分清。4. 踩坑实录我实际碰到的四个典型问题4.1 控件突然找不到探针脚本救了我最让人头疼的问题是昨天脚本还能跑今天启动后突然报错找不到控件。排查思路是按顺序来先看微信是不是自动升级了很多次都是微信静默更新之后控件树结构变了再看微信窗口标题是不是不对有没有弹窗把主窗口盖住最后跑一遍探针脚本重新打印控件树看搜索框和输入框的名称、类型有没有变化。如果只是名称变化直接改代码里的title_re就行。如果是结构大变整个控件树都不一样了那就老老实实对照新输出的控件树重新定位 Edit 控件的位置。我个人有个习惯每次脚本跑通就把探针脚本的输出保存一份等微信更新后再对比能省不少排查时间。还有一个容易被忽略的问题如果你是用管理员权限运行终端而微信是以普通权限启动的Windows 的 UI Automation 可能抓不到微信窗口。这个时候用管理员身份重新登录微信或者用普通权限运行脚本让两边的权限一致即可。4.2 type_keys输入中文乱码这个坑我印象太深了。最开始我用edit.type_keys(你好世界)结果消息发出去是“ä½ å¥½”完全没法看。原因很简单type_keys 的按键模拟机制对 Unicode 字符支持不好中文字符到达微信输入框时已经被转成了错误编码。解决办法就是源码里的那套剪贴板方案。先copy_to_clipboard(msg)再发^v再发回车。剪贴板里存的是一段 Unicode 文本粘贴过程不经过按键编码转换中文、表情符号、换行符都能原样发出去。这里有一个注意事项如果消息文本里包含多行粘贴后不会自动发送必须等回车那一下。我的代码里在 CtrlV 和 Enter 之间加了time.sleep(INPUT_DELAY)就是为了给粘贴操作留够时间防止回车太快粘贴还没完成就发出去了。4.3 焦点被抢占和弹窗干扰脚本跑着跑着微信突然弹出新闻弹窗、群聊消息提醒甚至手机端确认登录的提示框把输入框焦点给抢走了。结果就是脚本把消息发到了错误的窗口或者压根没发出去。这个问题在长时间运行时特别明显。我的处理方案有三层。第一层连接窗口后调用win.set_focus()把微信窗口拉到前台同时把任务栏里其它无关窗口的干扰降到最低。第二层在打开联系人和点击输入框前都重新定位控件不要缓存控件的引用每次操作都在当前窗口树里重新查找。第三层发送前用win.child_window(...).exists()判断输入框是否存在如果不存在就重新走一遍 open_contact 逻辑。这几招配合下来大部分焦点问题都能解决。如果电脑上通知应用特别多还有一种办法发送期间把微信窗口保持置顶。pywinauto 里可以通过win.set_window_pos或者调用 Windows API 实现不过我觉得有点过度设计了普通场景关闭弹窗 重新聚焦就够了。4.4 微信安全限制与合规提醒我必须在最后强调一下安全风险。微信对自动化操作的态度一直是明确反对的我自己的测试账号就遇到过这样的情况连续高频发送消息后微信要求做设备验证出现了“验证此设备和pc连接至相同网络”之类的提示。这类提示属于账号安全保护机制说明系统认为当前操作有异常触发了安全策略。遇到这种情况正确做法是立即停止脚本按微信的提示手动完成验证然后恢复手动操作。千万不要尝试写脚本绕过这个验证一旦被判定为恶意自动化轻则限制登录重则封号得不偿失。那些打着“稳定防封”旗号的第三方方案本质上都是钻空子今天能用明天可能就翻车。所以我的建议是这套方案只用于个人合法、低频的场景比如给自己发提醒、给同事发通知、测试环境发告警。单账号每天自动化发送控制在两位数以内发送间隔保持在 1 秒以上批量发送联系人数量控制在 5 个以内。真正有业务级消息发送需求请走官方 API这才是合规的路。最后再分享一个我一直在用的小习惯跑自动化的时候把微信窗口固定在屏幕左侧把脚本发送频率控制在 1 秒以上。这样既稳定又不影响我干别的。这套用 pywinauto 做 GUI 自动化的思路本质上不局限于微信凡是 Windows 桌面程序只要你能用探针脚本抓到控件都能用同样的方法自动化起来。脚本失灵时先别急着加延时先看控件树变了没有这比什么都管用。
返回列表