微信自动化进阶:用 `uiautomation` 和 `uiautomation2` 打造你的智能小助手 (自动收发消息)

微信自动化进阶:用 uiautomationuiautomation2 打造你的智能小助手 (自动收发消息)

在上一篇文章中,我们探讨了如何使用 Python 的 uiautomation (PC端) 和 uiautomation2 (Android端) 库来实现微信消息的自动发送。这次,我们将更进一步,让我们的Python脚本不仅能发送消息,还能监控新消息、提取内容,并根据预设逻辑自动回复,就像一个简单的客服机器人或个人助理。

本文目标

  1. 回顾消息自动发送的基础。
  2. 学习如何使用 uiautomationuiautomation2 检测微信新消息。
  3. 掌握从聊天界面提取最新消息内容和发送者(初步)。
  4. 实现基于关键词的简单自动回复逻辑。
  5. 构建一个持续运行的微信自动收发消息脚本。

为何需要自动收发消息?

  • 简单客服: 自动回复常见问题 (FAQ),引导用户。
  • 信息筛选与通知: 监控特定群聊或联系人的消息,当出现关键词时,自动回复或转发给指定的人。
  • 离线自动应答: 当你不在时,礼貌性地回复收到的消息。
  • 趣味互动: 创建一个简单的聊天机器人与朋友互动。

核心思路

无论是PC端还是Android端,自动收发消息的基本流程是相似的:

  1. 启动并登录微信。 (手动或脚本控制)
  2. 循环监控:
    a. 检测未读消息: 扫描聊天列表,寻找有未读标记的会话。
    b. 选择会话: 如果有未读,选择第一个未读会话进入。
    c. 提取消息: 获取当前聊天窗口的最新一条(或几条)消息。
    d. 判断来源: 确保是对方发来的消息,而不是自己刚发出的。 (这是关键且有挑战的一点)
    e. 处理消息: 根据消息内容,决定是否回复以及回复什么。
    f. 发送回复: 调用之前实现的发送消息逻辑。
    g. 标记已读/返回: 通常进入会话并回复后,消息会自动标记为已读。然后返回聊天列表继续监控。
  3. 适当延时: 在每次循环之间加入延时,避免CPU占用过高和操作过快。

准备工作

与上一篇文章相同,请确保:

  • Python 环境 已配置。
  • 微信客户端 已安装 (PC 或 Android)。
  • 库已安装:
    • pip install uiautomation pyperclip (Windows)
    • pip install uiautomation2 weditor (Android)
  • 辅助工具: Inspect.exe (Windows), weditor (Android) 用于查看UI元素属性。
  • Android环境: ADB 可用,USB调试已开启。

一、PC端微信自动收发 (uiautomation)

PC端微信的挑战在于其UI元素属性可能不那么稳定,且未读消息的标记方式、消息内容的提取需要仔细观察。

增强逻辑:

  1. 检测未读消息:
    • PC微信的聊天列表项,未读消息通常会有红点数字或仅红点提示。这些提示本身也是UI元素,可以尝试定位它们。
    • 或者,未读会话的列表项Name属性可能会包含未读消息数,例如 “文件传输助手[1]”。
  2. 提取最新消息:
    • 进入聊天后,消息区域是一个列表。最新消息通常在最底部。
    • 消息内容可能是 TextControlDataItemControl
    • 难点:区分消息是对方发的还是自己发的。 可以通过消息元素在界面上的左右位置、是否有头像、或消息元素父控件的某些特征来尝试区分。对于简单实现,可以假设进入未读会话后,最新的那条就是对方发的。
  3. 防止重复回复/回复自己:
    • 记录下自己最后发送的消息,避免对自己的消息进行响应。
    • 或者,在回复后,等待一小段时间再检查新消息,期望对方的消息在此之后到达。

示例代码 (PC 端 - 概念性增强):

import uiautomation as auto
import time
import pyperclip

# --- 复用上一篇文章的发送函数 ---
def send_wechat_message_pc(wechat_window, message: str):
    """在当前打开的聊天窗口发送消息 (简化版)"""
    input_box = wechat_window.EditControl(searchDepth=20) # 假设已在聊天窗口,输入框是最后一个Edit
    if not input_box.Exists(1,0.1):
        print("错误:PC - 未找到当前聊天窗口的输入框。")
        return False
    
    pyperclip.copy(message)
    input_box.SendKeys('{Ctrl}v', waitTime=0.1)
    time.sleep(0.2)
    
    send_button = wechat_window.ButtonControl(Name='发送')
    if send_button.Exists(1,0.1):
        send_button.Click(simulateMove=False)
    else: # 尝试回车发送
        input_box.SendKeys('{Enter}', waitTime=0.1)
    print(f"PC - 已发送消息: {message}")
    return True

# --- 新增的回复逻辑 ---
def get_reply_message(received_msg: str) -> str | None:
    """根据收到的消息生成回复 (简单关键词匹配)"""
    received_msg_lower = received_msg.lower()
    if "你好" in received_msg_lower or "hello" in received_msg_lower:
        return "你好!很高兴为您服务。"
    elif "价格" in received_msg_lower or "多少钱" in received_msg_lower:
        return "我们的产品价格请参考官网 XXX.com/pricing。"
    elif "帮助" in received_msg_lower or "help" in received_msg_lower:
        return "请问有什么可以帮助您的吗?您可以描述具体问题。"
    elif "再见" in received_msg_lower or "拜拜" in received_msg_lower:
        return "再见!"
    # 默认不回复,返回None
    return None

# --- 主监控循环 ---
def main_pc_auto_reply():
    print("PC微信自动回复程序启动...")
    wechat_window = auto.WindowControl(ClassName='WeChatMainWndForPC')

    if not wechat_window.Exists(5, 1):
        print("错误:未找到微信主窗口。请确保微信已登录并打开。")
        return

    wechat_window.SetTopmost(True) # 方便调试
    
    # 获取左侧聊天列表 Pane
    # 这个控件的Name属性可能为空,或者叫“会话”,需要用Inspect.exe确认
    # 假设聊天列表是一个 ListControl
    # chat_list_pane = wechat_window.ListControl(searchDepth=5) # 根据实际情况调整
    # 如果聊天列表没有明确的ListControl,可以尝试获取包含聊天项的Pane
    # 微信更新后,聊天列表可能在名为 "会话" 的 Pane 下
    session_pane = wechat_window.PaneControl(Name='会话') # 较新版微信
    if not session_pane.Exists(2,0.5):
        # 尝试旧版可能的父控件
        # chat_list_parent = wechat_window.Control(...) # 需要用Inspect找到聊天列表的直接父容器
        print("PC - 未找到聊天列表容器。脚本可能需要更新。")
        # 尝试点击 "聊天" 按钮确保在聊天列表界面
        chat_button = wechat_window.ButtonControl(Name="聊天")
        if chat_button.Exists(1,0.1):
            chat_button.Click(simulateMove=False)
            time.sleep(0.5)
        # 再次尝试找会话面板
        session_pane = wechat_window.PaneControl(Name='会话')
        if not session_pane.Exists(2,0.5):
            print("PC - 仍然找不到聊天列表容器。退出。")
            wechat_window.SetTopmost(False)
            return


    processed_chats_today = set() # 简单防止对同一会话在短时间内重复处理(更复杂需要基于时间戳)
    last_replied_message_content = {} # key: chat_name, value: last_replied_to_message

    while True:
        try:
            # 0. 确保焦点在微信,并返回到聊天列表的顶层(如果适用)
            # wechat_window.Click() # 点击一下窗口,确保激活
            # auto.SendKeys('{Esc}') # 有时按Esc可以返回列表,但要小心副作用
            # time.sleep(0.5)

            # 1. 寻找未读消息
            # 聊天列表项通常是 ListItemControl
            # 未读标记可能是子控件 TextControl(Name包含数字) 或 ImageControl (红点)
            # 或者ListItem的Name属性可能包含未读数,如 "张三[2]"
            
            # 优先确保在聊天列表界面
            chat_button = wechat_window.ButtonControl(Name="聊天")
            if chat_button.Exists(1,0.1) and not chat_button.IsSelected():
                chat_button.Click(simulateMove=False)
                time.sleep(0.5) # 等待列表加载
            
            unread_chat_item = None
            # 遍历会话列表中的所有ListItem
            # 这里的定位非常依赖微信版本和UI结构
            chat_items = session_pane.GetChildren() # 获取Pane下的所有子项
            
            potential_unread_items = []
            for item in chat_items:
                if item.ControlTypeName == 'ListItemControl':
                    # 检查是否有红点数字提示 (通常是一个TextControl作为子元素)
                    # 微信的红点元素比较难直接抓取,因为它可能没有稳定的Name或AutomationId
                    # 我们可以检查ListItem的Name属性是否包含 '[数字]' 这样的模式
                    # 或者检查是否有特定的子元素指示未读
                    # 例如,未读消息的ListItem下可能会有一个特定的子元素,其Name是数字
                    # red_dot_texts = item.TextControl(searchDepth=2) # 查找子Text控件
                    # for rd_text in red_dot_texts:
                    #     if rd_text.Name and rd_text.Name.isdigit(): # 如果名字是纯数字
                    #         unread_chat_item = item
                    #         break
                    # 另一种方式:检查 Name 属性,如 "联系人名称[1]"
                    if "[" in item.Name and item.Name.endswith("]"):
                        # 进一步确认是未读标记,而不是群成员数量等
                        content_in_bracket = item.Name[item.Name.rfind("[")+1:-1]
                        if content_in_bracket.isdigit(): # 确保是数字
                            potential_unread_items.append(item)
                            # print(f"PC - 发现潜在未读: {item.Name}")
                # if unread_chat_item:
                #     break
            
            # 从潜在未读中选择一个处理 (例如第一个)
            if potential_unread_items:
                unread_chat_item = potential_unread_items[0] # 取第一个
                chat_name_full = unread_chat_item.Name # 如 "张三[2]"
                # 提取纯粹的聊天名称
                chat_name = chat_name_full.split('[')[0] if '[' in chat_name_full else chat_name_full
                print(f"PC - 检测到未读消息来自: {chat_name_full}")

                # 避免短时间内重复处理同一个已回复的对话的第一条消息
                if chat_name in processed_chats_today and chat_name not in last_replied_message_content:
                    print(f"PC - {chat_name} 今日已处理过,且无新消息记录,跳过。")
                    time.sleep(5) # 避免快速循环同样内容
                    continue

                # 2. 点击进入聊天
                unread_chat_item.Click(simulateMove=False)
                time.sleep(1) # 等待聊天内容加载

                # 3. 提取最新消息
                # 聊天记录通常在 ScrollBar控件内部的List或Pane里
                # 最新消息在最下方。消息项可能是GroupControl, ListItemControl或CustomControl
                # 这是一个非常棘手的部分,因为消息结构复杂
                # 假设聊天记录在一个ListControl中,每个消息是一个ListItemControl
                message_list_control = wechat_window.ListControl(searchDepth=15) # 需要用Inspect工具确认
                if not message_list_control.Exists(2,0.5):
                    print("PC - 未找到消息列表控件。")
                    auto.SendKeys('{Esc}') # 尝试返回聊天列表
                    time.sleep(1)
                    continue
                
                messages = message_list_control.GetChildren()
                if not messages:
                    print(f"PC - {chat_name} 聊天窗口没有消息内容。")
                    auto.SendKeys('{Esc}') # 尝试返回聊天列表
                    time.sleep(1)
                    continue

                # 最新消息通常是最后一个,但要判断是不是对方发的
                # 简化:取倒数第一个文本内容不为空的ListItem的Name
                last_message_text = None
                for i in range(len(messages) - 1, -1, -1):
                    msg_item = messages[i]
                    # 尝试获取消息文本,消息文本可能在Name属性,也可能在子TextControl中
                    current_msg_text = msg_item.Name 
                    # 很多消息控件的Name属性可能为空,文本在更深层的子控件
                    # 可以尝试找 msg_item 下的 TextControl
                    # text_children = msg_item.findAll(controlType=auto.ControlType.TextControl, searchDepth=1)
                    # if text_children:
                    #    current_msg_text = "".join([child.Name for child in text_children])
                    
                    if current_msg_text and current_msg_text.strip():
                        # 还需要判断这条消息是不是自己发的
                        # 这是一个难点。PC版微信UI上区分不明显。
                        # 简单策略:如果这条消息和我们上次对这个会话回复的消息不同,就认为是新消息
                        if chat_name in last_replied_message_content and \
                           last_replied_message_content[chat_name] == current_msg_text:
                            print(f"PC - {chat_name} 的最新消息 '{current_msg_text}' 与上次回复的相同,跳过。")
                            last_message_text = None # 表示没有新消息要处理
                            break 
                        last_message_text = current_msg_text
                        break
                
                if last_message_text:
                    print(f"PC - 收到来自 {chat_name} 的消息: {last_message_text}")

                    # 4. 获取回复
                    reply = get_reply_message(last_message_text)
                    if reply:
                        # 5. 发送回复
                        if send_wechat_message_pc(wechat_window, reply):
                            last_replied_message_content[chat_name] = last_message_text # 记录回复过的消息
                            processed_chats_today.add(chat_name) # 标记今天处理过
                        else:
                            print(f"PC - 向 {chat_name} 发送回复失败。")
                    else:
                        print(f"PC - 对于消息 '{last_message_text}' 无预设回复。")
                        # 即使不回复,也标记为已处理,避免下次还当新消息
                        last_replied_message_content[chat_name] = last_message_text
                        processed_chats_today.add(chat_name)

                else:
                    print(f"PC - 未能从 {chat_name} 提取到有效的新消息。")
                    # 认为这个未读提示是旧的或者无法解析
                    if chat_name not in processed_chats_today: # 如果之前没处理过,现在也标记下
                        processed_chats_today.add(chat_name)


                # 6. 返回聊天列表 (或者等待下一个循环自动处理)
                # 通常点击其他聊天或顶部的“聊天”按钮会返回
                # 也可以尝试发送Esc键
                # auto.SendKeys('{Esc}')
                # time.sleep(0.5)
                # 点击聊天按钮,确保返回列表界面,以便扫描其他未读
                chat_button_nav = wechat_window.ButtonControl(Name="聊天")
                if chat_button_nav.Exists(1,0.1):
                     chat_button_nav.Click(simulateMove=False)
                time.sleep(1)


            else:
                print(f"PC - 当前无未读消息... ({time.strftime('%Y-%m-%d %H:%M:%S')})")
            
            # 清理一天前的processed_chats_today, 避免集合过大 (简单实现)
            if len(processed_chats_today) > 100: # 假设一天不会处理超过100个不同对话
                 processed_chats_today.clear()
                 last_replied_message_content.clear()


        except Exception as e:
            print(f"PC - 发生错误: {e}")
            auto.PrintControl(wechat_window, depth=3) # 打印当前窗口结构,帮助调试
            # 尝试恢复
            try:
                if wechat_window.Exists(1,0.1) and not wechat_window.IsTopmost(): # 如果窗口还在但不是最前
                    wechat_window.SetTopmost(True)
                # 可以尝试按 ESC 返回到稳定状态
                auto.SendKeys('{Esc}')
                time.sleep(1)
                auto.SendKeys('{Esc}') # 多按几次
                time.sleep(1)
                # 点击聊天按钮尝试恢复
                chat_btn_recover = wechat_window.ButtonControl(Name="聊天")
                if chat_btn_recover.Exists(1,0.1):
                    chat_btn_recover.Click(simulateMove=False)

            except Exception as e_recover:
                print(f"PC - 恢复操作时出错: {e_recover}")


        time.sleep(10) # 每10秒检查一次
    
    wechat_window.SetTopmost(False)


# main_pc_auto_reply() # 取消注释以运行PC版

PC端 uiautomation 实现要点与难点:

  • UI元素稳定性: 这是最大的挑战。微信PC版更新可能导致控件的Name, ClassName, AutomationId 改变。需要频繁使用 Inspect.exeautomation.py -t 3 来调试和更新定位器。
  • 未读标记: 不同版本的微信,未读消息的红点或数字提示的UI元素可能完全不同,甚至难以直接定位。有时需要通过父级ListItem的Name属性中是否包含 [数字] 来间接判断。
  • 消息归属: 判断最新消息是对方发的还是自己发的,在PC版UI上直接获取此信息比较困难。上面的示例代码做了一些简化处理。更可靠的方法可能需要分析消息控件的布局属性(如X坐标判断左右)或者其子元素中是否有头像控件等,但这会使代码更复杂且更易受UI变更影响。
  • 滚动问题: 如果聊天列表或消息列表过长,可能需要模拟滚动才能找到所有元素。
  • 焦点管理: 确保操作时微信窗口是激活状态,且焦点在正确的控件上。

二、Android端微信自动收发 (uiautomation2)

Android端借助 weditor 查看元素属性会方便很多,但同样面临UI变化的问题。

增强逻辑:

  1. 检测未读消息:
    • 微信主界面的聊天列表,未读消息通常有红色圆圈和数字。这些是 TextView 控件,可以通过 resourceId (可能不稳定) 或其 text (数字) 来定位。
  2. 提取最新消息:
    • 进入聊天界面后,消息列表通常是 RecyclerViewListView
    • 最新消息在列表底部。每个消息项是一个布局,包含头像、发送者昵称(群聊中)、消息气泡等。
    • 消息内容通常在 TextView (resourceId 如 com.tencent.mm:id/b47 或类似,会变!)。
    • 判断消息归属: Android版微信,对方消息和自己消息的布局通常不同(例如,对方在左,自己在右)。可以通过消息项根布局的子元素 resourceId 来区分。例如,自己发的消息可能有一个特定的已发送状态图标,而对方消息没有。或者消息气泡的背景 resourceId 不同。
  3. 防止重复回复: 与PC端类似,记录已回复的消息或对话。

示例代码 (Android 端 - 概念性增强):

import uiautomator2 as u2
import time

# --- 复用上一篇文章的发送函数 ---
def send_wechat_message_android(d, message: str):
    """在当前Android微信聊天界面发送消息 (简化版)"""
    # 假设已在聊天界面
    # 输入框ID,用weditor查找,例如 com.tencent.mm:id/auj (会变!)
    input_field_id = "com.tencent.mm:id/auj" 
    if not d(resourceId=input_field_id).exists(timeout=3):
        print("错误:Android - 未找到聊天输入框。")
        return False
    
    d(resourceId=input_field_id).set_text(message)
    time.sleep(0.5)
    
    # 发送按钮ID或Text,用weditor查找,例如 com.tencent.mm:id/ay5 或 text="发送" (会变!)
    send_button_selector = {"text": "发送", "className": "android.widget.Button"}
    if not d(**send_button_selector).exists(timeout=2):
        # 有时发送按钮是 ImageView
        send_button_selector = {"resourceId": "com.tencent.mm:id/ay5"} # 举例
        if not d(**send_button_selector).exists(timeout=2):
            print("错误:Android - 未找到发送按钮。")
            return False
            
    d(**send_button_selector).click()
    print(f"Android - 已发送消息: {message}")
    return True

# --- 回复逻辑 (与PC版共用) ---
def get_reply_message(received_msg: str) -> str | None:
    """根据收到的消息生成回复 (简单关键词匹配)"""
    received_msg_lower = received_msg.lower()
    if "你好" in received_msg_lower or "hello" in received_msg_lower:
        return "你好呀!我是机器人小助手。"
    elif "你是谁" in received_msg_lower:
        return "我是一个基于uiautomation2的Python程序。"
    elif "图片" in received_msg_lower:
        return "抱歉,我暂时还不能处理图片哦。"
    elif "再见" in received_msg_lower or "拜拜" in received_msg_lower:
        return "再见啦!"
    return None # 默认不回复

# --- 主监控循环 ---
def main_android_auto_reply(device_serial=None):
    print("Android微信自动回复程序启动...")
    try:
        d = u2.connect(device_serial) if device_serial else u2.connect()
        d.app_start("com.tencent.mm", stop=True) # 确保从干净状态启动或切换到前台
        print(f"已连接到设备: {d.device_info['serial']},等待微信加载...")
        time.sleep(7) # 等待微信完全启动并加载主界面

        # 用weditor获取这些ID
        # 微信主界面下方 "微信" tab 的ID (确保在聊天列表页)
        wechat_tab_id = "com.tencent.mm:id/dub" # 示例ID,会变! (text="微信")
        # 聊天列表项中未读消息红点数字的ID
        unread_badge_id = "com.tencent.mm:id/o_f" # 示例ID,会变! (通常是个TextView)
        # 聊天列表项联系人名称的ID
        contact_name_id = "com.tencent.mm:id/b5o" # 示例ID,会变!
        # 进入聊天后,消息列表的ID (通常是RecyclerView或ListView)
        # message_list_id = "com.tencent.mm:id/b26" # 示例ID (RecyclerView)
        # 消息内容TextView的ID (区分对方和自己可能需要更复杂的选择器)
        message_content_id = "com.tencent.mm:id/b47" # 示例ID,非常容易变!
        # 返回按钮的description或ID
        back_button_desc = "返回" 

        processed_message_texts = {} # key: contact_name, value: set of processed message texts

        while True:
            try:
                # 0. 确保在微信主界面的聊天列表
                # 如果不在,尝试点击 "微信" tab
                if not d(resourceId=wechat_tab_id, text="微信").exists(timeout=2) and \
                   not d(description="当前所在页面,与对应底部标签为微信").exists(timeout=1): # 有些版本的微信tab没有固定id,但有这个desc
                    print("Android - 不在主聊天列表,尝试返回或重启微信。")
                    d.press("back") # 尝试返回
                    time.sleep(1)
                    d.press("back") # 多按几次
                    time.sleep(1)
                    if not d(resourceId=wechat_tab_id, text="微信").exists(timeout=2):
                        d.app_start("com.tencent.mm", stop=True)
                        time.sleep(7)
                # 点击"微信"确保在聊天列表 (如果它不是selected状态)
                # 有时微信tab是用description定位, e.g. d(description="微信").click()
                # 假设微信tab的父控件可选
                # wechat_tab_element = d(resourceId=wechat_tab_id, text="微信")
                # if wechat_tab_element.exists and not wechat_tab_element.parent().selected: # 检查父控件是否被选中
                #    wechat_tab_element.click()
                #    time.sleep(1)


                # 1. 寻找未读消息
                # 查找带有未读标记的聊天项
                # 未读标记是一个TextView, 其父容器是聊天列表项
                unread_chat_item_parent = None
                # 查找所有未读红点
                unread_indicators = d(resourceId=unread_badge_id) 
                
                if not unread_indicators.exists:
                    print(f"Android - 当前无未读消息... ({time.strftime('%Y-%m-%d %H:%M:%S')})")
                    time.sleep(15)
                    d.swipe_ext("up", scale=0.7, duration=0.5) # 下拉刷新一下列表
                    time.sleep(2)
                    continue

                # 处理第一个可见的未读消息
                # uiautomator2的元素选择器返回的是一个集合,即使只有一个元素
                # 我们需要迭代或者取第一个
                found_specific_unread = False
                for indicator_ui_object in unread_indicators: # 遍历所有匹配的未读标记
                    # 通过未读标记向上找到整个聊天项 (通常是父级的父级等,结构依赖weditor观察)
                    # 假设未读标记的直接父级或爷爷级是可点击的聊天项
                    # chat_item_candidate = indicator_ui_object.parent() # .parent().parent() 等
                    # 这个定位非常依赖实际的UI层次结构
                    # 一个更通用的方法是,如果未读标记和联系人名在同一个父容器下
                    # 那么
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值