
简介这是一份面向Python初学者与微信自动化爱好者的实战型技术资料围绕“微信防撤回”这一具体场景展开帮助读者理解如何借助itchat等库监听并留存好友、群聊及公众号中可撤回的消息类型。资源包共1个文件为PDF格式大小约49KB内容以代码讲解与实现思路为主便于在电脑端对照阅读和调试。目前已有258人学习说明该主题在Python小工具开发领域具有一定关注度。资料中涉及文本、图片、语音、视频、位置、名片、分享、附件等多类消息的接收与缓存处理并包含消息字典结构设计、临时文件目录管理、超时消息清理等关键逻辑读者可从中掌握消息监听注册、发送人识别、群聊信息提取以及撤回提示与消息ID不一致等细节问题的处理方式。整体适合作为Python网络编程与微信自动化方向的练手项目参考也可为后续扩展消息持久化、界面展示或跨平台适配提供基础思路。1. 拆开一份 Python 微信防撤回脚本它到底拦住了什么微信里最让人抓狂的场景之一就是消息刚弹出来还没来得及看清对方一个撤回聊天框只剩一行灰字。这份 Python 微信防撤回神器干的事情很直接在消息到达本机的那一刻先落一份缓存等撤回通知进来时把缓存里的原文、图片、语音、视频重新发到文件传输助手。它不是什么黑科技本质是消息监听加本地暂存再配合撤回事件做一次回捞。适合谁看写过一点 Python、想搞明白 itchat 这类微信自动化库怎么接消息、怎么处理文件类型的人。也适合想拿它当练手项目理解消息 ID、群聊发送人识别、临时文件清理这几个关键点的人。需要先说明这类脚本依赖网页版微信协议能不能登录成功、能稳定跑多久取决于账号和当前协议状态不是代码写完就一定能用。下面按“先跑通、再拆解、最后避坑”的顺序讲。2. 环境准备与最小可运行版本把 itchat 登录和消息监听跑起来2.1 依赖安装与目录初始化这份脚本依赖 itchat 和它内部封装的网页微信接口Python 版本建议 3.6 到 3.8太新的版本在部分依赖上容易出兼容问题。先建虚拟环境再装依赖避免污染全局。# 建一个独立虚拟环境Python 3.7 兼容性相对稳 python3 -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install itchat # 如果安装慢可以指定国内镜像源 pip install itchat -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后脚本里有一处路径必须改就是临时文件目录。原文写死的是作者本机路径直接跑会在别人机器上报目录不存在。import os # 临时目录改成自己机器上真实存在的路径 rev_tmp_dir ./wechat_rev_tmp/ if not os.path.exists(rev_tmp_dir): os.mkdir(rev_tmp_dir) else: # 启动时清空上次残留避免旧文件干扰 for f in os.listdir(rev_tmp_dir): path os.path.join(rev_tmp_dir, f) if os.path.isfile(path): os.remove(path)这里逻辑说明一下os.mkdir只在目录不存在时创建else分支负责清理历史文件。参数上rev_tmp_dir建议用相对路径或绝对路径都行但一定要有写权限。图片、语音、视频这些消息itchat 会把文件下载到本地msg[Text](rev_tmp_dir msg[FileName])这一句就是触发下载并把文件存到临时目录文件名取自消息自带的FileName。2.2 登录与消息注册itchat 的登录入口是auto_loginhotReloadTrue表示短时间内重启可以免扫码enableCmdQR2是在命令行里用字符画二维码方便没有图形界面的环境。import itchat from itchat.content import * if __name__ __main__: # hotReload 让短时间内重启不用重复扫码 itchat.auto_login(hotReloadTrue, enableCmdQR2) itchat.run()参数说明hotReload依赖本地生成的登录状态文件默认在当前目录删掉它就得重新扫码。enableCmdQR2里的数字控制二维码在终端里的显示宽度终端太窄会显示错乱可以改成 1 或 3 试。登录成功后itchat.run()会阻塞主线程持续接收消息。这一步能跑通说明环境和协议登录没问题后面才是消息处理逻辑。2.3 消息类型与可撤回范围不是所有消息都能撤回脚本里注册的类型是 TEXT、PICTURE、MAP、CARD、SHARING、RECORDING、ATTACHMENT、VIDEO、FRIENDS、NOTE。其中 NOTE 是系统通知撤回提示就走这个类型。注册时带上isFriendChat、isGroupChat、isMpChat表示好友、群聊、公众号消息都监听。itchat.msg_register([TEXT, PICTURE, MAP, CARD, SHARING, RECORDING, ATTACHMENT, VIDEO, FRIENDS, NOTE], isFriendChatTrue, isGroupChatTrue, isMpChatTrue) def handler_reveive_msg(msg): # 每条消息进来先记时间用于后续超时清理 msg_time_rev time.strftime(%Y-%m-%d %H:%M:%S, time.localtime()) msg_id msg[MsgId] ...MsgId是消息的唯一标识撤回通知里带的msgid就是拿它去缓存字典里查原消息。这里有个容易忽略的点不同消息类型的MsgId长度不一样脚本后面用len(old_msg_id) 11来判断是不是表情类消息因为表情消息和撤回提示的 ID 对不上这是 itchat 的一个已知现象原文里用face_bug变量单独兜了一下。3. 消息缓存与撤回回捞msg_dict 结构和文件消息处理3.1 缓存字典的字段设计脚本用一个全局字典msg_dict存所有收到的消息key 是MsgIdvalue 是一组字段。这个结构决定了后面能不能正确还原撤回内容。msg_dict {} # 单条消息缓存结构 msg_dict.update({ msg_id: { msg_from: msg_from, # 发送人群聊里是实际发送人 msg_time: msg_time, # 消息原始时间戳用于超时判断 msg_time_rev: msg_time_rev, # 接收时间展示用 msg_type: msg[Type], # 消息类型 msg_content: msg_content, # 文本内容或文件名 msg_share_url: msg_share_url,# 分享链接 group_name: group_name # 群名私聊为 None } })字段说明msg_time是微信服务器给的时间戳单位秒用来算消息是否超过 120 秒msg_time_rev是本地接收时间展示给人看msg_content对文本类直接存文字对文件类存的是文件名真正的文件在临时目录里。group_name只有群消息才有值私聊是 None后面拼撤回提示时会判断。3.2 群聊发送人识别群聊消息里msg[FromUserName]是群 ID不是发送人。要拿到具体是谁发的得用ActualUserName去好友列表里比对备注名。def set_sender_group_chat(msg): msg_from msg[ActualNickName] friends itchat.get_friends(updateTrue) from_user msg[ActualUserName] for f in friends: if from_user f[UserName]: # 优先用备注名没有备注再用昵称 msg_from f[RemarkName] or f[NickName] break groups itchat.get_chatrooms(updateTrue) for g in groups: if msg[FromUserName] g[UserName]: group_name g[NickName] break return {msg_from: msg_from, group_name: group_name}逻辑说明get_friends(updateTrue)会拉一次最新好友列表updateTrue保证备注名是新的。参数上RemarkName可能为空字符串所以用or兜到NickName。群列表同理用群 ID 反查群昵称。这一步在好友多的时候会有点慢因为每次群消息都遍历一遍常见优化是把好友和群列表缓存起来隔一段时间再刷新。3.3 撤回通知的匹配与回发撤回提示是 NOTE 类型消息内容里带 CDATA 包裹的“撤回了一条消息”以及被撤回消息的 msgid。脚本用正则把 msgid 抠出来再去msg_dict里查。itchat.msg_register([NOTE], isFriendChatTrue, isGroupChatTrue, isMpChatTrue) def send_msg_helper(msg): global face_bug if re.search(r!\[CDATA\[.*撤回了一条消息\]\], msg[Content]) is not None: old_msg_id re.search(msgid(.*?)/msgid, msg[Content]).group(1) old_msg msg_dict.get(old_msg_id, {}) if len(old_msg_id) 11: # 表情消息 ID 对不上用 face_bug 兜底 itchat.send_file(rev_tmp_dir face_bug, toUserNamefilehelper) os.remove(rev_tmp_dir face_bug) else: msg_body old_msg.get(msg_from) 撤回了 old_msg.get(msg_type) \ 消息\n old_msg.get(msg_time_rev) \n old_msg.get(msg_content) if old_msg.get(group_name) is not None: msg_body old_msg.get(group_name) msg_body if old_msg[msg_type] Sharing: msg_body \n old_msg.get(msg_share_url) itchat.send(msg_body, toUserNamefilehelper) # 文件类消息单独再发一次文件 if old_msg[msg_type] in (PICTURE, RECORDING, VIDEO, ATTACHMENT): file fil%s % (rev_tmp_dir old_msg[msg_content]) itchat.send(msgfile, toUserNamefilehelper) os.remove(rev_tmp_dir old_msg[msg_content]) msg_dict.pop(old_msg_id)参数说明toUserNamefilehelper是文件传输助手所有回捞内容都发到这里不会打扰别人。文件类消息用fil前缀加路径的方式发送这是 itchat 发文件的约定格式。发完文件后os.remove删掉临时文件同时msg_dict.pop清掉缓存避免内存和磁盘越积越多。face_bug是全局变量存最近一条消息内容专门处理表情撤回 ID 对不上的情况。3.4 超时清理线程缓存不能无限增长脚本开了个子线程每轮遍历msg_dict把超过 120 秒的消息删掉文件类消息连临时文件一起删。import _thread def clear_timeout_msg(): need_del_msg_ids [] for m in msg_dict: msg_time msg_dict[m][msg_time] if int(time.time()) - msg_time 120: need_del_msg_ids.append(m) if len(need_del_msg_ids) 0: for i in need_del_msg_ids: old_msg msg_dict.get(i) if old_msg[msg_type] in (PICTURE, RECORDING, VIDEO, ATTACHMENT): os.remove(rev_tmp_dir old_msg[msg_content]) msg_dict.pop(i) # 主程序里启动子线程 _thread.start_new_thread(clear_timeout_msg)逻辑说明微信撤回时限是 2 分钟所以缓存保留 120 秒足够覆盖。_thread是底层线程模块简单够用但它是死循环式的一遍过实际跑的时候最好包一层while True加time.sleep否则只清理一次就退出了。参数上120 这个阈值可以按需调调大占内存调小可能漏掉临界撤回。4. 避坑与常见问题排查登录、路径、表情和文件删除4.1 登录失败或频繁掉线现象auto_login扫码后提示登录失败或者跑一会儿就掉线。原因itchat 依赖的网页微信协议对账号有风控新号、异地登录、频繁重启都容易触发。解决先用hotReloadTrue减少重复扫码登录状态文件别乱删如果一直失败换一个使用时间较长、日常活跃的账号试别用刚注册的小号硬刚。4.2 临时目录路径写死导致报错现象运行后报FileNotFoundError或PermissionError。原因原文里rev_tmp_dir是作者本机绝对路径换机器就不存在。解决改成自己机器上真实存在且有写权限的目录建议用相对路径./wechat_rev_tmp/并在启动时确保目录已创建。Windows 上路径分隔符用/或os.path.join都行别直接抄带盘符的路径。4.3 表情消息撤回后发不出来现象撤回的是表情文件传输助手收到的是空或者报错。原因表情消息的MsgId和撤回提示里的msgid长度、格式对不上msg_dict里查不到。解决脚本用face_bug存最近一条消息内容兜底但只能兜最近一条连续多条表情撤回会错位。常见做法是单独给表情消息建一个短队列按时间顺序匹配而不是只存一个变量。4.4 文件删除时报文件不存在现象os.remove抛FileNotFoundError。原因文件类消息下载失败或者超时清理线程已经删过一次撤回回捞时又删一次。解决删除前加os.path.exists判断或者用try/except包住。另外超时清理和撤回回捞可能并发操作同一个文件加个简单的锁或者用队列串行化更稳。4.5 群消息发送人显示成群昵称而非备注现象撤回提示里发送人显示的是群昵称不是好友备注。原因set_sender_group_chat里先取ActualNickName再遍历好友列表覆盖如果好友列表没刷新到或者ActualUserName对不上就保留了群昵称。解决确保get_friends(updateTrue)执行成功对不在好友列表里的群成员只能显示群昵称这是数据源限制不是 bug。5. 进阶玩法把撤回记录落库并做去重统计跑通基础版之后我一般会把撤回记录从“发到文件传输助手”升级成“落本地库”方便回溯和统计谁最爱撤回。用 SQLite 就够不用额外装服务。import sqlite3 conn sqlite3.connect(revoke_log.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS revoke_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg_id TEXT UNIQUE, -- 唯一约束防止重复插入 msg_from TEXT, group_name TEXT, msg_type TEXT, msg_content TEXT, revoke_time TEXT ) ) conn.commit() def save_revoke(old_msg_id, old_msg): try: cursor.execute( INSERT INTO revoke_log (msg_id, msg_from, group_name, msg_type, msg_content, revoke_time) VALUES (?, ?, ?, ?, ?, ?), (old_msg_id, old_msg.get(msg_from), old_msg.get(group_name), old_msg.get(msg_type), old_msg.get(msg_content), time.strftime(%Y-%m-%d %H:%M:%S, time.localtime())) ) conn.commit() except sqlite3.IntegrityError: # msg_id 重复说明已经记过直接跳过 pass参数说明msg_id加UNIQUE约束配合IntegrityError捕获解决同一条撤回被重复处理的问题。msg_content对文件类消息存的是文件名想存文件本身可以另建一个附件表用msg_id关联。落库之后统计谁撤回最多就是一句 SQLSELECT msg_from, COUNT(*) AS revoke_count FROM revoke_log GROUP BY msg_from ORDER BY revoke_count DESC LIMIT 10;验证方法手动发一条消息再撤回查revoke_log表里有没有对应记录连续撤回同一条如果平台允许看会不会重复插入。我踩过的坑是msg_id在极少数情况下会重复不加唯一约束会记出两条统计就偏了。从那以后我每次建表都先把唯一约束加上再写插入逻辑。希望帮到你。本文还有配套的精品资源点击获取