头图

在当今社交软件中,微信是最常用的通讯工具之一。然而,随着时间的推移,我们的好友列表中可能会出现一些不再活跃的账号,也就是我们俗称的“僵尸粉”。

这些账号可能是由于长时间不使用、账号被封禁或者故意将我们删除或拉黑。为了保持好友列表的清洁和有效沟通,同时也为了帮助我们更好地管理微信好友,最近我使用 Python 和 uiautomator2 库编写了一个自动化工具来清理这些僵尸粉。

这个工具会通过检测好友的状态(如是否被删除、是否被拉黑或是否账号出现问题)来批量标记并处理这些好友。

这个工具的主要功能包括:

  • 识别被删除或拉黑的好友:通过模拟转账操作,检查与好友的交易是否正常。
  • 标记问题账号:对于账号存在问题(如被封禁、拉黑、删除)的好友进行标记。
  • 记录和输出结果:将检查结果记录到文件中,方便后续查看和管理。

接下来,我将从代码的整体结构开始分析,介绍如何使用 uiautomator2 来控制 Android 设备,并通过自动化方式清理微信中的僵尸粉。

需要注意的是:因为我手头上只有一部 OPPO Reno4 Pro 安卓手机,因此只能在这部手机上做了实验。不太确定是否在其他机型上有无问题。

核心类和初始化

这段代码定义了一个名为 WXCleanFriends 的类,该类包含了所有执行清理操作的核心方法。类内部包含多个常量和状态标记,用于表示不同的好友状态,如正常、被删除、被拉黑等。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
微信清理僵尸粉工具
通过遍历微信好友列表,将僵尸粉(被删除、被拉黑、账号问题)进行标记
"""
import json
import time

import uiautomator2 as u2
from uiautomator2 import Direction


class WXCleanFriends:
    """
    微信清理僵尸粉
    运行设备为:OPPO Reno4 pro
    """

    # 检查好友状态时,如果是被对方删除的话,需要打上的标签
    TAG_HAS_DELETED = '清粉-被删除'
    # 检查好友状态时,如果是被对方拉黑的话,需要打上的标签
    TAG_HAS_BLACK = '清粉-被拉黑'
    # 检查好友状态时,如果是对方账号出现问题的话,需要打上的标签
    TAG_ACCOUNT_PROBLEM = '清粉-账号问题'

    # 好友状态
    FRIEND_STATUS_NORMAL = 'normal'  # 正常
    FRIEND_STATUS_HAS_DELETED = 'has_deleted'  # 被删除
    FRIEND_STATUS_HAS_BLACK = 'has_black'  # 被拉黑
    FRIEND_STATUS_ACCOUNT_PROBLEM = 'account_problem'  # 账号问题
    FRIEND_STATUS_UNKNOWN = 'unknown'  # 未知

    # 给好友打标签情况
    TAG_NEVER_REMARK = 'never_remark'  # 从来没有打过标签
    TAG_HAS_JOIN = 'has_join'  # 已经加入过该标签群组
    TAG_HAS_REMARK_OTHER = 'has_remark_other'  # 已经打过标签,但不是【{tag_name}】标签

    def __init__(self,
                 last_friend_wx_code: str,
                 ignore_wx_code: list,
                 max_page_get_friend_list: int = 8
                 ):
        """
        :param last_friend_wx_code: 通讯录中最后一个好友的微信号
        :param ignore_wx_code: 需要被忽略检测的微信号或者微信昵称
        :param max_page_get_friend_list: 检查通讯录中的好友列表最大页数
        """

        # 连接设备
        self.d = u2.connect()
        # 被检查的好友列表(可以理解成所有的好友)
        self.friends_list = []
        # 记录当前已经被检测过的好友,避免重复检测
        self.friends_has_checked = {}

        # 通讯录中最后一个好友的微信号
        self.last_friend_wx_code = last_friend_wx_code
        # 需要被忽略检测的微信号或者微信昵称
        self.ignore_wx_code = ignore_wx_code
        self.max_page_get_friend_list = max_page_get_friend_list

    def enable_debug(self):
        """
        开启调试模式
        :return:
        """

        # 开启日志
        # u2.enable_pretty_logging()
        # 设置 http 请求超时时间
        u2.HTTP_TIMEOUT = 60
        # 开启调试模式
        # self.d.debug = True

        print(f"设备信息 ==> {self.d.info}")
        print(f"设备IP ==> {self.d.wlan_ip} 设备号 ==> {self.d.serial}")

        # 设置查找元素等待时间,单位秒
        self.d.implicitly_wait(30)

        # UI 层次结构
        # xml = d.dump_hierarchy(compressed=False, pretty=True, max_depth=50)
        # print(xml)

初始化方法接受三个参数:

  • last_friend_wx_code:通讯录中最后一个好友的微信号,用于确定清理到哪个位置停止。
  • ignore_wx_code:一个列表,包含了需要跳过检查的微信号或昵称。
  • max_page_get_friend_list:最多获取多少页的好友列表,避免检测过多的好友。

自动化打开微信

    def weixin_start(self):
        """
        打开微信
        :return:
        """
        # self.d.xpath('//android.widget.TextView[@text="微信"]').click(5)

        wx_app = self.d(text='微信', className='android.widget.TextView')
        if wx_app.exists() is False:
            print('当前页面没有微信 APP,请切换到有微信 APP 的页面')
            exit(1)

        print("打开微信")
        wx_app.click(timeout=5)

weixin_start 方法用于启动微信应用。它通过 uiautomator2 来模拟点击操作。如果当前页面没有找到微信应用,程序将退出。

获取好友信息和状态判断

    def get_personal_info(self):
        """
        获取好友个人信息
        :return:
        """
        remark = nickname = wx_code = zone = tag = ''

        remark_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cf8')
        if remark_elem.exists(timeout=3):
            remark = remark_elem.get_text()

        nickname_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cf7')
        if nickname_elem.exists(timeout=3):
            nickname = nickname_elem.get_text().lstrip('昵称:  ')

        wx_code_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cff')
        if wx_code_elem.exists(timeout=3):
            wx_code = wx_code_elem.get_text().lstrip('微信号:  ')

        zone_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cf6')
        if zone_elem.exists(timeout=3):
            zone = zone_elem.get_text().lstrip('地区:  ')

        tag_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cd4')
        if tag_elem.exists(timeout=3):
            tag = tag_elem.get_text()

        print(
            f"备注名是[{remark}] 昵称是[{nickname}] 微信号是[{wx_code}] 地区是[{zone}] 标签是[{tag}]")

        return {'remark': remark, 'nickname': nickname, 'wx_code': wx_code, 'zone': zone, 'tag': tag}

get_personal_info 方法通过定位 UI 元素来提取好友的个人信息,包括备注名、昵称、微信号、地区和标签等。

    def judge_friend_status(self) -> str:
        """
        判断当前好友的状态
        :return:
        """
        # 点击【发消息】按钮
        if self.d(text='发消息').click_exists(10) == False:
            # if self.d(resourceId='com.tencent.mm:id/cfb').click_exists(timeout=10) == False:
            print('没有点击【发消息】按钮')
            exit()
        # 点击【➕】
        if self.d(resourceId='com.tencent.mm:id/bjz').click_exists(timeout=10) == False:
            print('没有点击【➕】按钮')
            exit()
        # 点击【转账】按钮
        if self.d(resourceId='com.tencent.mm:id/a12', text='转账').click_exists(timeout=10) == False:
            print('没有点击【转账】按钮')
            exit()

        time.sleep(1)

        # 先清空转账金额,以免导致金额输入错误或者输入大额金额
        self.d(resourceId='com.tencent.mm:id/pbn').clear_text(3)
        # 输入 0.01 元 ---> start
        self.d(resourceId='com.tencent.mm:id/keyboard_0').click_exists(3)
        self.d(resourceId='com.tencent.mm:id/keyboard_dot').click_exists(3)
        self.d(resourceId='com.tencent.mm:id/keyboard_0').click_exists(3)
        self.d(resourceId='com.tencent.mm:id/keyboard_1').click_exists(3)
        # 输入 0.01 元 ---> end
        time.sleep(1)
        # # 点击【转账】按钮
        self.d(resourceId='com.tencent.mm:id/keyboard_action', text='转账').click_exists(3)

        # 点击转账之后,就可以根据页面元素来判断当前好友的状态

        # 1、如果页面中存在【¥0.01】元素,证明当前好友没有将自己删除
        is_normal = self.d(text='¥0.01', className='android.widget.TextView').exists(timeout=3)
        if is_normal is True:
            return self.FRIEND_STATUS_NORMAL

        # 判断是否有弹窗
        alert_elem = self.d(resourceId='com.tencent.mm:id/jlg')
        if alert_elem.exists(timeout=5):
            time.sleep(2)
            # 有弹窗的情况下,通过弹窗中的文本内容来判断当前好友的状态
            alert_text = alert_elem.get_text()
            # 2、判断是否被拉黑
            if '请确认你和他(她)的好友关系是否正常' in alert_text:
                return self.FRIEND_STATUS_HAS_BLACK
            # 3、判断是否被删除
            if '你不是收款方好友,对方添加你为好友后才能发起转账' in alert_text:
                return self.FRIEND_STATUS_HAS_DELETED
            # 4、判断是否被限制登录(对方的账号可能出现了问题、该账号已无法使用)
            if ('对方微信号已被限制登录,为保障你的资金安全,暂时无法完成交易。' in alert_text
                    or '当前使用人数过多,请稍后再试。' in alert_text
                    or '对方账户有异常行为,已被限制收款,本次交易无法完成。对方可通过微信支付公众号上收到的消息查看详情。' in alert_text
            ):
                return self.FRIEND_STATUS_ACCOUNT_PROBLEM

        # 5、其他情况(未知)
        return self.FRIEND_STATUS_UNKNOWN

judge_friend_status 方法通过一系列点击操作模拟转账行为,根据页面弹窗判断好友的状态。主要判断的状态包括:

  • 正常状态:好友未删除,未拉黑,账号正常。
  • 被删除:如果弹出提示“你不是收款方好友”,则说明好友已删除。
  • 被拉黑:如果出现“请确认你和他(她)的好友关系是否正常”,则说明好友将你拉黑。
  • 账号问题:如果弹窗提示账户已被限制登录,则说明对方账号存在问题。

标签管理

    def has_join_tag_group(self, tag_name: str) -> str:
        """
        判断当前用户是否已经被打了某个标签
        :param tag_name: 标签名称 eg: 清粉-被删除
        :return: never_remark: 从来没有打过标签
                    has_join: 已经加入过该标签群组
                    has_remark_other: 已经打过标签,但不是【{tag_name}】标签
        """
        # 好友资料页面中的【标签】属性
        tag_elem = self.d(className='android.widget.TextView', resourceId='com.tencent.mm:id/cd4')

        if tag_elem.exists(timeout=5) is False:
            print(f'没有找到标签元素,证明没有对该好友打过任何标签,现在需要对该好友打上【{tag_name}】标签')
            # 证明没有给该好友打过任何标签
            return self.TAG_NEVER_REMARK

        # 获取标签元素的文本
        tags = tag_elem.get_text()
        if tag_name in tags:
            print(f'已经加入过【{tag_name}】群组')
            # 证明已经加入过该标签群组
            return self.TAG_HAS_JOIN
        else:
            # 证明已经打过标签,但不是【{tag_name}】标签
            print(f'已经打过标签,但不是【{tag_name}】标签,现在需要对该好友打上【{tag_name}】标签')
            return self.TAG_HAS_REMARK_OTHER

    def join_tag_group(self, tag_name: str):
        """
        加入标签群组
        :param tag_name: 标签名称 eg: 清粉-被删除
        :return:
        """
        print(f'开始加入【{tag_name}】标签群组')

        # 这里有 2 种情况:
        # 一种是之前加过标签的,那么则有“标签”;
        # 一种是没有加过标签的,那么则有“设置备注和标签”
        # 好友资料页面中的【标签】
        tag_zone = self.d(resourceId='com.tencent.mm:id/cd5', text='标签')
        if tag_zone.exists(timeout=5) is False:
            print('之前没有给该好友打过任何标签')
            tag_zone = self.d(className='android.widget.TextView', resourceId='android:id/title', text='设置备注和标签')
            if tag_zone.exists(timeout=5) is False:
                print('没有找到【设置备注和标签】按钮')
                exit()

        tag_zone.click(timeout=1)
        # 【设置备注和标签】页面中的【标签】去添加标签
        self.d(resourceId='com.tencent.mm:id/cd8').click(timeout=1)

        # 在标签列表中查找是否有【{tag_name}】标签
        target_tag_elem = self.d(text=tag_name, className='android.widget.TextView')

        if target_tag_elem.exists(timeout=1) is False:
            print(f"没有找到【{tag_name}】标签,现在需要创建【{tag_name}】标签")
            # 点击【新建标签】按钮
            self.d(resourceId='com.tencent.mm:id/k70', text='新建标签').click(timeout=1)
            create_tag = self.d(resourceId='com.tencent.mm:id/d98', text='标签名称')
            create_tag.clear_text(timeout=1)
            create_tag.set_text(tag_name, 1)
            time.sleep(1)
            self.d(resourceId='com.tencent.mm:id/kao', text='确定').click(timeout=1)
            time.sleep(1)
            self.d(resourceId='com.tencent.mm:id/fp', text='保存').click(timeout=1)
            time.sleep(1)
            self.d(resourceId='com.tencent.mm:id/fp', text='完成').click(timeout=1)
        else:
            print(f"已经存在【{tag_name}】标签,现在只需要添加")
            target_tag_elem.click(timeout=1)
            time.sleep(1)
            self.d(resourceId='com.tencent.mm:id/fp', text='保存').click(timeout=1)
            time.sleep(1)
            self.d(resourceId='com.tencent.mm:id/fp', text='完成').click(timeout=1)

has_join_tag_group 方法用于判断好友是否已经被打上某个标签,比如“清粉-被删除”。根据返回值,程序决定是否为好友添加新标签。

针对不同的好友状态进行后续的操作

    def when_has_deleted(self):
        """
        如果当前好友已经将自己删除时
        :return:
        """
        self.d(resourceId='com.tencent.mm:id/mm_alert_ok_btn', text='我知道了').click_exists(timeout=3)

        # 1、退出【输入法】页面
        # self.d.press("back")
        # time.sleep(1)

        # 1、退出转账页面
        self.d.press("back")
        time.sleep(1)

        # 2、退出【红包、转账、语音输入、我的收藏……】页面
        self.d.press("back")
        time.sleep(1)

        # 3、退出聊天页面
        self.d.press("back")
        time.sleep(1)

        tag_status = self.has_join_tag_group(self.TAG_HAS_DELETED)
        if tag_status != self.TAG_HAS_JOIN:
            self.join_tag_group(self.TAG_HAS_DELETED)

    def when_has_black(self):
        """
        如果当前好友已经将自己拉黑时
        :return:
        """
        self.d(resourceId='com.tencent.mm:id/mm_alert_ok_btn', text='我知道了').click_exists(timeout=3)

        # # 1、退出【输入法】页面
        # self.d.press("back")
        # time.sleep(1)

        # 2、退出转账页面
        self.d.press("back")
        time.sleep(1)

        # 3、退出【红包、转账、语音输入、我的收藏……】页面
        self.d.press("back")
        time.sleep(1)

        # 4、退出聊天页面
        self.d.press("back")
        time.sleep(1)

        tag_status = self.has_join_tag_group(self.TAG_HAS_BLACK)
        if tag_status != self.TAG_HAS_JOIN:
            self.join_tag_group(self.TAG_HAS_BLACK)

    def when_account_problem(self):
        """
        如果当前好友的账号出现问题时
        :return:
        """
        self.d(resourceId='com.tencent.mm:id/mm_alert_ok_btn', text='我知道了').click_exists(timeout=3)

        # # 1、退出【输入法】页面
        # self.d.press("back")
        # time.sleep(1)

        # 2、退出转账页面
        self.d.press("back")
        time.sleep(1)

        # 3、退出【红包、转账、语音输入、我的收藏……】页面
        self.d.press("back")
        time.sleep(1)

        # 4、退出聊天页面
        self.d.press("back")
        time.sleep(1)

        tag_status = self.has_join_tag_group(self.TAG_ACCOUNT_PROBLEM)
        if tag_status != self.TAG_HAS_JOIN:
            self.join_tag_group(self.TAG_ACCOUNT_PROBLEM)

    def when_normal(self):
        """
        如果当前好友是正常状态时
        :return:
        """
        # 1、退出【输入支付密码】页面
        self.d.press("back")
        time.sleep(1)

        # 2、退出【输入法】页面
        self.d.press("back")
        time.sleep(1)

        # 3、退出转账页面
        self.d.press("back")
        time.sleep(1)

        # 4、退出【红包、转账、语音输入、我的收藏……】页面
        self.d.press("back")
        time.sleep(1)

        # 5、退出聊天页面
        self.d.press("back")
        time.sleep(1)

当我们判断清楚了每一位好友的状态之后,我们还需要退回到通讯录页面,方便继续检测下一位好友。但是极有可能每一个状态返回到通讯录中的步骤可能不一样,因此,我们就最好是根据不同的状态来分别处理。

接下来就是最重要的步骤了,通过遍历通讯录中的每一个好友,来检测每一位好友的状态如何。

循环检查每个好友

    def check_every_friend(self):
        run_status = 'doing'
        time.sleep(3)
        elems = self.d(resourceId='com.tencent.mm:id/kbq')

        for elem in elems:
            print()

            time.sleep(1)
            friend_nickname = elem.get_text(timeout=10)
            # 点击进入好友详情页面
            print(f'进入好友详情页面 --> {friend_nickname}')
            # 判断是否需要忽略检测
            if friend_nickname in self.ignore_wx_code:
                print(f"可以直接忽略检测【{friend_nickname}】")
                continue

            elem.click(timeout=5)

            # 获取好友个人信息
            personal_info = self.get_personal_info()

            # 判断是否需要忽略检测
            if personal_info['wx_code'] in self.ignore_wx_code or personal_info['nickname'] in self.ignore_wx_code:
                print(f"忽略检测【{personal_info['nickname']}】【{personal_info['wx_code']}】")
                self.d.press("back")
                continue

            # 判断当前好友是否已经被检测过了
            if personal_info['wx_code'] in self.friends_has_checked:
                print(f"已经被检测过了,跳过检测【{personal_info['nickname']}】【{personal_info['wx_code']}】")
                self.d.press("back")
                continue

            # 判断当前好友的状态
            status = self.judge_friend_status()
            if status == self.FRIEND_STATUS_HAS_DELETED:
                self.when_has_deleted()
            elif status == self.FRIEND_STATUS_HAS_BLACK:
                self.when_has_black()
            elif status == self.FRIEND_STATUS_ACCOUNT_PROBLEM:
                self.when_account_problem()
            elif status == self.FRIEND_STATUS_NORMAL:
                self.when_normal()
            else:
                print(f'当前好友状态未知 {status}')
                exit()

            # 将当前好友的【状态】和【个人信息】数据进行合并
            personal_info['status'] = status
            print(personal_info)
            # 存储当前好友的信息
            write_content_to_file('./friends_info.log', personal_info)

            # 记录所有被检测好友的信息
            self.friends_list.append(personal_info)
            # 记录已经被检测的好友信息
            self.friends_has_checked[personal_info['wx_code']] = personal_info

            # 判断是否已经检查到最后一个好友
            if personal_info['wx_code'] == self.last_friend_wx_code:
                print('已经检查到最后一个好友')
                run_status = 'done'
                break

            time.sleep(1)
            # 退回到通讯录列表页面(重要!!!)
            self.d.press("back")

        return run_status

    def all_friends_check(self):
        """
        开始检查所有好友
        :param self:
        :return:
        """
        # 检查是否有设置最后一个好友的微信号
        if self.last_friend_wx_code == '':
            print('请先设置最后一个好友的微信号')
            return

        # 至少将自己的微信号加入到忽略检测列表中(因为自己不能给自己转账)
        if len(self.ignore_wx_code) == 0:
            print('至少将自己的微信号加入到忽略检测列表中,否则会发生报错')
            return

        # 根据设定的最大页数,循环对通讯录中的每一个好友进行检查
        i = 0
        while True:
            i += 1
            print(f"这是第 {i} 次翻页")
            if i >= self.max_page_get_friend_list:
                break

            run_status = self.check_every_friend()
            if run_status == 'done':
                break

            time.sleep(1)
            # 一页检查完了之后,向上滑动
            # self.d.swipe(100, 1000, 100, 500)
            self.d.swipe_ext("up", scale=0.9)
            # 向上滑动之后,等待一会儿
            time.sleep(2)

        print('所有好友检查完毕')

check_every_friend 方法遍历好友列表,进入每个好友的详情页面,根据好友的状态进行相应的处理。如果好友状态符合删除、拉黑或账号问题,程序会为其打上对应标签。

让代码跑起来

核心代码已经写完了,剩下的步骤就比较简单了,让代码跑起来就行了

def write_content_to_file(file_path: str, content):
    """
    将内容写入文件
    :param file_path:
    :param content:
    :return:
    """
    content_json = json.dumps(content, ensure_ascii=False)
    with open(file_path, 'a', encoding='utf-8') as file_to_write:
        file_to_write.write(content_json + '\n')


if __name__ == '__main__':
    last_friend_wx_code = '123'

    # 需要被忽略检测的微信号或者微信昵称
    # 这里至少将自己的微信号加入到忽略检测列表中(因为自己不能给自己转账)
    # 并且也必须保留以下【微信团队】和【文件传输助手】
    ignore_wx_code = ['微信团队', '文件传输助手']

    max_page_get_friend_list = 8

    wx_clean = WXCleanFriends(last_friend_wx_code, ignore_wx_code, max_page_get_friend_list)

    # wx_clean.test_run()
    # exit()

    # 开启调试模式
    wx_clean.enable_debug()

    # 打开微信
    # wx_clean.weixin_start()
    # 点击【通讯录】
    # wx_clean.open_contacts()
    # 让通讯录页面加载完毕
    # time.sleep(3)

    # 开始检查所有好友
    wx_clean.all_friends_check()

    friends_list = wx_clean.get_friends_list()
    write_content_to_file('./friends_list.json', friends_list)
    print(friends_list)

总结

这款微信清理僵尸粉工具使用 Python 和 uiautomator2 库通过自动化方式帮助我们批量清理微信好友中的不活跃或无效好友。

通过检查好友的状态并为其打上标签,工具不仅提高了清理效率,也避免了人工逐个操作的繁琐。对于开发者而言,这个项目展示了如何结合 Python 与自动化工具进行高效的设备操作和应用管理。

如果你也有类似的需求,不妨也玩一玩……

这篇文章只是将不同状态的好友打上了标签,下一篇文章详解自动化删除指定标签中的所有好友,可以观望一下。😄


左诗右码
85 声望11 粉丝

三观比五官更正,思想比套路更深。常用技术栈PHP、Go、Python,享受编程,平时爱好写点文章。V公主号:「左诗右码」,欢迎关注交流。