ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Python和pywinauto实现微信消息自动发送

用Python和pywinauto实现微信消息自动发送 老实说最开始写这个脚本是因为我实在顶不住每天早上的“机械劳动”打开电脑微信找到联系人把同样的内容复制粘贴一遍又一遍。不管是给客户回统一订单状态还是给几个工作群同步项目进度谁发谁知道真的又慢又容易漏。后来我花了一个下午用Python加上pywinauto这个GUI自动化库把整条链路跑通了。这套脚本能做的很直接——替你打开微信里指定联系人的聊天窗口把消息内容填进去按回车发送全程不需要你碰鼠标。如果你也经常要重复发通知、发日报、发公告或者想从身边的小工具开始学Python自动化这篇内容正好适合你。完整源码、调试思路、踩过的坑我都会写出来照着抄基本就能跑。1. 这个脚本到底解决什么问题1.1 需求场景与痛点拆解我接触到的需求通常离不开这几类场景第一类是定时汇报型。每天早上一到工位要把昨天的数据、日报、进度同步给负责人或者几个核心群。内容其实就那几句但因为要精准发给固定的人又担心漏发只能老老实实挨个点开窗口粘贴发送。这个流程看着不难但一旦有十几个联系人手速再快也得十来分钟中间被打断一下就容易发串。第二类是运营分发型。做社群运营或者活动通知需要把同一段公告同时发给多个群、多个联系人。手动复制粘贴几十次人很容易麻而且越到后面越容易点错会话框。用脚本跑一遍遍历名单自动发速度和准确性都能上一个层次。第三类是测试验证型。有些做客服系统、聊天机器人测试的同学需要频繁向某个微信会话发消息来验证功能手打太慢脚本可以按预设内容稳定触发。这些需求背后的本质是同一个重复、有规律、高频次的GUI操作。而自动化要做的就是把“找到会话-聚焦输入框-输入内容-触发发送”这几步用程序固化下来让人只出脑子不出手。1.2 为什么选pywinauto而不是其他方案我最早其实也考虑过别的路子各有各的问题简单对比一下方案原理优点缺点推荐度微信官方接口 / 企业微信API调用官方开放能力合规、稳定个人微信拿不到这样的接口只能走企业应用有条件优先用协议逆向 / 抓包模拟模拟客户端协议发消息可以完全后台运行、速度快门槛高账号风控风险极大不推荐图像识别模拟点击OpenCV截屏找图、点坐标不依赖窗口结构分辨率一变就废电脑被遮挡就乱点应急可以日常脆pyautogui全局键鼠屏幕坐标模拟键盘鼠标写起来快容易误触窗口必须在前台适合临时操作pywinautoWindows UI自动化操作控件面向控件而非坐标稳定性好微信版本升级后控件结构可能变化需要重新抓取推荐我最终选pywinauto核心原因是它面向控件树而不是屏幕坐标。说白了你不用关心“发送按钮在屏幕的哪个像素位置”只需要告诉程序“请找到那个叫发送按钮的控件点它”。这样窗口位置变了、分辨率换了脚本依然能跑。另外一个重要原因是pywinauto拿到窗口句柄之后可以把窗口激活到前台再操作比纯坐标点击靠谱得多。而且它对比协议逆向最大的优势是安全——你用的是正常界面操作只是把键盘鼠标动作自动化了不是去破解或者伪造什么数据包风险边界完全不一样。2. 核心原理pywinauto是怎么代替我们操作电脑的2.1 两种后端机制win32与uiapywinauto的底层有两种工作模式分别叫win32后端和uia后端很多人第一次接触时容易搞混这里展开说一下。win32后端走的是传统Windows消息机制也就是通过Win32 API向控件发送消息比如直接设置文本框内容、发送按钮点击消息。它的优点是非常快资源占用低适合那些用传统Win32控件写出来的老软件。缺点也很明显一旦程序用自绘界面、内嵌网页或者现代GUI框架比如WPF、Electron、CEFwin32后端能看到的东西就非常有限经常只能拿到一个光秃秃的窗口外壳内部元素全丢。uia后端走的是微软UI Automation框架通过COM接口和程序交互能识别到的控件信息更完整很多自绘元素的辅助层也会暴露出来。代价是速度稍微慢一些但现代电脑上这个差别几乎可以忽略。微信从早期版本到现在界面一直在往自绘和混合渲染方向走。老的3.x版本里win32后端勉强还能拿到一些编辑框控件到了4.0之后的版本win32能拿到的控件树少得可怜很多东西都成了一块画布。所以我的建议非常直接不要纠结一律用backenduia。2.2 微信窗口结构分析与定位思路要做自动化第一步是理解目标窗口长什么样。微信PC版的主窗口标题是会变的有时直接叫“微信”有时会显示成“某个联系人 - 微信”所以连接窗口时用正则title_re.*微信.*是最稳的做法。从控件树的角度看微信主窗口可以粗略分为左侧导航栏、会话列表区、中间消息记录区、右下角消息输入区。我们真正关心的其实只有几个关键点搜索框用来输入联系人名字调出对应会话。但不同版本它是否是一个独立控件并不确定。消息输入区输入文本的地方通常是Edit控件但新版里也不一定好找。发送按钮点击触发发送。同样会随版本变化。正因如此我的最终实现没有死磕控件名而是走“快捷键调起搜索 - 输入联系人 - 回车进入会话 - 粘贴内容 - 回车发送”这条通用路径。控件树只用来确认窗口存在、拿到窗口对象而不是逐节点去点按钮。这也是这套脚本能跨微信版本稳定运行的最关键原因。你再想想如果你硬coding一个child_window(title发送, control_typeButton)万一哪天微信把按钮名字从“发送”改成“Send”或者干脆改成图标脚本就废了。用快捷键和通用编辑能力就绕开了这个坑。3. 环境准备与调试利器3.1 安装Python和依赖包环境要求不复杂就是Windows系统加Python 3.8以上版本。Python没装的话去官网下载安装包安装时记得勾选“Add Python to PATH”不然后面在命令行里敲python会提示找不到命令。下载慢的话可以用国内镜像源这个网上随手能搜到不赘述。装好Python之后打开命令行安装依赖pip install pywinauto pyperclippywinauto是GUI自动化的主库pyperclip用来操作剪切板。安装完成后验证一下python -c import pywinauto; print(pywinauto.__version__)能输出版本号就说明环境没问题。这里多说一句pywinauto只能在Windows环境跑Linux和macOS都不支持因为底层依赖的是Windows的UI自动化框架。另外微信也必须是Windows桌面版不是网页版也不是mac版别搞混了。3.2 用print_control_identifiers透视微信控件树调试GUI自动化最痛苦的事情就是“不知道这个控件叫什么名字”。pywinauto提供了一个非常实用的方法print_control_identifiers()可以把当前窗口下所有能识别到的控件信息打印出来。先手动打开微信并登录然后在任务管理器里找到微信进程的PID。你可以右键任务栏选择“任务管理器”到“详细信息”标签页找WeChat.exe对应的PID那一列。拿到PID之后跑这段调试脚本from pywinauto import Application pid int(input(请输入微信的PID: )) app Application(backenduia).connect(processpid) win app.window(title_re.*微信.*) win.wait(exists ready, timeout10) with open(wechat_ctrl_tree.txt, w, encodingutf-8) as f: f.write(win.print_control_identifiers()) print(控件树已保存到 wechat_ctrl_tree.txt)这段脚本会生成一个文本文件里面是当前微信窗口的完整控件树。你打开之后用CtrlF搜Edit能看到当前有哪些输入框搜Button能看到有哪些按钮。什么时候需要这个就是当你发现快捷键方案在某些版本上失灵、必须精确定位某个控件时用它来查名字最直接。另外一个调试技巧建议在微信窗口没有最小化的时候抓取控件树。窗口一旦最小化到托盘uia后端常常拿不到子控件列表这不算bug是Windows UI自动化本身的限制。4. 完整源码实现4.1 最稳的输入方案剪贴板加快捷键在贴完整源码之前必须先说一个我在实际开发里踩得最深、也最影响成败的坑中文输入。很多第一次写pywinauto的人看到网上教程说“可以用type_keys模拟键盘输入”于是兴冲冲地写type_keys(你好世界)结果运行一看输入框里出现的是????或者干脆没反应。原因很简单type_keys本质上是模拟键盘按键它对ASCII字符支持很好但一旦遇到中文这种非ASCII字符它就无能为力了因为它不知道该怎么把这个字符拆成键盘按键。网上有些方案推荐用SendKeys库我实测下来也不太稳定时灵时不灵。折腾一圈之后我发现最稳的办法就是绕开键盘直接操作剪贴板用pyperclip.copy(text)把消息文本写进系统剪贴板。在微信输入框里按快捷键CtrlV粘贴。这个方法天然支持任意文本内容中文、英文、数字、符号统统没问题而且不用关心键盘输入法的状态。做GUI自动化的人一定记住这个思路遇到模拟键盘搞不定的字符第一时间想剪贴板方案。4.2 核心代码搜索联系人并自动发送消息完整源码来了。我当前的实现思路是通过tasklist命令找到微信进程的PID并连接。激活微信主窗口。按CtrlF调出微信的搜索框。输入联系人名字按回车进入对应会话。把消息内容放进剪贴板粘贴到输入框按回车发送。直接贴可以跑的代码import subprocess import time from pywinauto import Application, keyboard import pyperclip def find_wechat_process(): 通过 tasklist 命令查找 WeChat.exe 的 PID for encoding in (gbk, utf-8): try: output subprocess.check_output(tasklist, shellTrue, encodingencoding, errorsignore) break except Exception: continue for line in output.splitlines(): if WeChat.exe in line: parts line.split() return int(parts[1]) # PID 是第二列 return None def connect_wechat(retry3): 连接到已运行的微信返回主窗口对象。 如果微信没开或者没登录成功会返回 None。 for _ in range(retry): pid find_wechat_process() if pid: try: app Application(backenduia).connect(processpid) win app.window(title_re.*微信.*) win.wait(exists ready, timeout10) return win except Exception as e: print(f连接微信窗口失败: {e}) time.sleep(1) return None def open_contact(win, contact): 在微信主窗口内打开指定联系人/群聊的会话窗口 win.restore() win.set_focus() time.sleep(0.5) # 调起微信搜索框 keyboard.send_keys(^f) time.sleep(0.5) # 清空可能存在的旧内容再输入联系人名字 keyboard.send_keys(^a) keyboard.send_keys(contact) time.sleep(1) # 回车进入匹配到的第一个会话 keyboard.send_keys({ENTER}) time.sleep(1) def send_message_to(contact, message): 给指定联系人发送消息 win connect_wechat() if win is None: print(没有找到微信窗口请确认微信已启动并登录成功) return False open_contact(win, contact) # 把消息写入剪贴板粘贴到输入框后发送 pyperclip.copy(message) time.sleep(0.3) keyboard.send_keys(^v) time.sleep(0.5) keyboard.send_keys({ENTER}) time.sleep(0.5) print(f消息已发送给: {contact}) return True if __name__ __main__: # 单聊示例 send_message_to(文件传输助手, 这是一条自动发送的测试消息)这段代码可以解决80%的需求。我特意把窗口激活、搜索、发送分开成函数方便你按需扩展。4.3 一条龙调用与批量发送演示实际使用中更多需求是“给一批人发同样的内容”。批量发送的代码并不复杂本质上就是循环调用send_message_to。但有个原则必须守每发一个人间隔至少1到3秒别一口气不喘地发。批量场景我推荐间隔更长后面我会讲原因。批量发送示例def batch_send(contacts, message, interval3): 给一批联系人发送消息间隔默认3秒 win connect_wechat() if win is None: print(没有找到微信窗口请确认微信已启动并登录成功) return for idx, contact in enumerate(contacts, start1): print(f正在处理 {idx}/{len(contacts)}: {contact}) open_contact(win, contact) pyperclip.copy(message) time.sleep(0.3) keyboard.send_keys(^v) time.sleep(0.5) keyboard.send_keys({ENTER}) time.sleep(interval) print(批量发送完成) if __name__ __main__: contacts [项目A群, 项目B群, 财务对接] batch_send(contacts, 本周周报已提交请查收。, interval5)这里有个细节值得注意open_contact里每次都会重新执行搜索联系人也就是说即使当前已经在某个人的聊天窗口也会先切走再搜回来。这个设计看似多了一步但避免了“当前窗口不是目标联系人却直接发消息”的严重事故——宁可慢一点也要确保对象正确。5. 踩坑实录与排查技巧5.1 微信版本差异老脚本为什么突然失效这个问题我估计很多人遇到过网上找到一段很老的pywinauto自动发送代码当时人家用得好好的自己一跑就报ElementNotFoundError或者干脆定位不到任何控件。原因基本就是微信版本升级了。PC微信3.x时代窗口的控件结构还算“传统”很多老代码会这样定位输入框app.window(class_name_re.*WeChatMainWndForPC.*).child_window(title输入, class_nameEdit)这套写法在3.x的部分小版本里是能用的。但是微信4.0之后界面大改大量使用自绘控件和CEF渲染窗口的类名变了内部的输入区也不是一个简简单单的Edit控件了。于是所有依赖旧控件名的代码集体失效。我的应对方案就是第4章那套“快捷键剪贴板”的思路。无论微信怎么改界面搜索联系人的快捷键逻辑基本不会变复制粘贴更是操作系统层面的能力所以这套脚本在未来一段时间内都会有比较好的兼容性。5.2 微信没启动、没登录、窗口被隐藏怎么处理运行脚本最常见的第一类报错是连接失败典型提示是找不到进程、找不到窗口等。排查顺序如下先确认微信确实启动了。如果没启动find_wechat_process会返回None脚本会提示你手动打开。再确认微信登录成功了。脚本能连接进程但如果是停留在扫码登录页窗口标题可能不匹配也会失败。最后看窗口是否被最小化到托盘。窗口最小化到托盘时句柄还在但子控件可能获取不到所以我在open_contact里先调用了win.restore()把窗口复原再做后续操作。如果你希望脚本能自动拉起微信可以加一个辅助函数import os def ensure_wechat_running(): 如果微信没运行尝试从常见路径启动 if find_wechat_process(): return True common_paths [ rC:\Program Files (x86)\Tencent\WeChat\WeChat.exe, rC:\Program Files\Tencent\WeChat\WeChat.exe, rD:\Program Files\Tencent\WeChat\WeChat.exe, ] for path in common_paths: if os.path.exists(path): subprocess.Popen(path) print(微信已启动请手动扫码登录) return True print(找不到微信安装路径请手动打开微信) return False注意微信登录本身必须人工扫码脚本没办法替你完成身份验证。所以这个辅助函数只能启动程序登录这个步骤还是要人来点。5.3 中文输入乱码和焦点错乱中文乱码是高频问题解决办法前面已经讲了一定走剪贴板不要走type_keys。但如果用了剪贴板还是粘贴不进去或者粘贴到了奇怪的地方那大概率是焦点错乱了。我遇到过的情况是这样的微信搜索框调出来后如果联系人名字输完、回车进入了会话微信默认会把焦点放在消息输入框里这时直接粘贴回车都能正常。但如果你手动操作过微信或者脚本运行过程中被中断过焦点可能停在搜索框这时候按CtrlV就会把内容粘到搜索框里去不但发不出去还把搜索内容搞乱了。应对方法有两条路进入会话后先按一下CtrlA全选输入框里的内容如果有旧内容的话再粘贴。如果从快捷键无法确定焦点位置就退而求其次用child_window(control_typeEdit)去定位输入框并调用它的set_focus()方法。另外一个让我很头疼的问题微信设置里的“使用回车发送消息”选项。如果你的微信改成了“使用CtrlEnter发送”那么代码里最后那个{ENTER}就只会换行不会发送。所以跑脚本前请确认微信的发送快捷键设置。默认是回车发送但保险起见你可以在第一次测试时先发给“文件传输助手”验证一下。5.4 频率控制与账号安全提醒这个部分我必须认真写因为它比代码本身重要。GUI自动化本质上还是在模拟人的操作并不是微信官方开放的能力。如果你用脚本高频、大批量地群发消息非常容易触发微信的账号安全机制轻则被限制部分功能重则要求重新验证登录。这不是危言耸听我身边就有人为了给客户群发消息搞得太猛结果账号要反复验证工作直接受影响。我自己使用时会遵守这几条规则单个联系人之间的间隔至少1秒批量场景建议5秒以上。同一时段内发送总量控制在几十条以内不搞“脚本跑一晚上发几千条”这种事。发文内容必须人工先确认过尤其是涉及金额、时间、地址这类信息发错了很难撤回。优先用“文件传输助手”或者自己小号做一次完整测试再对真实联系人操作。自动化是工具工具本身没有对错但用的人和场景决定了风险边界。我的建议是把它用在提高自己工作效率上不要用来做骚扰别人的营销轰炸。6. 扩展玩法6.1 配置Windows定时任务实现无人值守如果你想实现“每天早上到工位脚本已经自动把日报发给相关人了”可以配合Windows任务计划程序。先写一个批处理文件假设叫run_send.bat内容很简单chcp 65001 nul cd /d %~dp0 python send_daily.pychcp 65001是防止Python输出中文时在控制台乱码cd /d %~dp0保证批处理在脚本所在目录运行。然后在Windows搜索框里打开“任务计划程序”新建任务。触发器设为每天上午9点操作指向run_send.bat“条件”选项卡里取消“只有在计算机使用交流电源时才启动”否则笔记本插着电源才能触发如果希望电脑睡眠时也能补跑还可以勾选“唤醒计算机运行此任务”。这里有个前提任务触发时微信必须已经在运行并且登录好。所以如果你电脑设置了开机自启微信这个方案就很顺。如果微信没登录脚本会提示你“找不到微信窗口”不会产生实际副作用最多就是当天没发出去。6.2 从Excel批量读取联系人自动分发有时候联系人名单存在Excel里比如“联系人”“消息内容”两列。用openpyxl读取后循环发送非常简单from openpyxl import load_workbook def send_from_excel(file_path, interval5): wb load_workbook(file_path) ws wb.active win connect_wechat() if win is None: print(没有找到微信窗口请确认微信已启动并登录成功) return for row in ws.iter_rows(min_row2, values_onlyTrue): contact str(row[0]).strip() content str(row[1]).strip() if not contact or not content: continue print(f发送给: {contact}) open_contact(win, contact) pyperclip.copy(content) time.sleep(0.3) keyboard.send_keys(^v) time.sleep(0.5) keyboard.send_keys({ENTER}) time.sleep(interval) wb.close() print(Excel分发完成)Excel里的消息内容一定要人工提前核对脚本不会替你判断“这条适不适合发给这个人”。批量场景里我把默认间隔设成了5秒宁可发得慢也别触动风控。我在实际使用中还有一个习惯就是所有新脚本上线前先加一个“演练模式”真实的联系人换成“文件传输助手”跑一遍全流程看看有没有异常确认没问题了再换成真实名单。这多花两分钟但能避免很多尴尬的翻车现场。
返回列表