手擼了一個全自動微信清粉小工具(原始碼詳解)

左诗右码發表於2024-12-01

在當今社交軟體中,微信是最常用的通訊工具之一。然而,隨著時間的推移,我們的好友列表中可能會出現一些不再活躍的賬號,也就是我們俗稱的“殭屍粉”。

這些賬號可能是由於長時間不使用、賬號被封禁或者故意將我們刪除或拉黑。為了保持好友列表的清潔和有效溝通,同時也為了幫助我們更好地管理微信好友,最近我使用 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 與自動化工具進行高效的裝置操作和應用管理。

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

這篇文章只是將不同狀態的好友打上了標籤,下一篇文章詳解自動化刪除指定標籤中的所有好友,可以觀望一下。😄

相關文章