Hello,我是新星博主:小恒不會java
背景
使用?wxpy
?或者?itchat
?這種第三方庫通過Python控制自己的微信號,實現(xiàn)很多自動化操作,用的是微信網(wǎng)頁版接口,不過隨著微信的發(fā)展(信息安全等方面愈加重要,這種不符合官方期望出現(xiàn)的東西,很容易就破產(chǎn)。也由于itchat在 python 的 request 請求中,使用到的 headers 都是非常簡單的 headers。而且頻繁利用到config.USER_AGENT
?,被發(fā)現(xiàn)懷疑用腳本封號是當(dāng)然的
所以我打算用wxauto,用UI控件的形式操作微信(uiautomation實現(xiàn))
wxauto第三方庫介紹
wxauto是某GitHub某位開源的
https://github.com/cluic/wxauto
功能很基礎(chǔ),畢竟是UI控件,實現(xiàn)的功能有
(1)群發(fā)消息包括圖片,文件
(2)自動回復(fù)和添加好友(有小bug,簡單修改一下就行)
(3) 監(jiān)控群聊人員或者個人聊天框
(4)獲取微信好友昵稱等信息(有bug,我改了一下沒成功)
效果如下
wxauto.py代碼
"""
Author: Cluic
Update: 2024-03-10
Version: 3.9.8.15
"""
import uiautomation as uia
from .languages import *
from .utils import *
from .elements import *
from .errors import *
from .color import *
import datetime
import time
import os
import re
try:
from typing import Literal
except:
from typing_extensions import Literal
class WeChat(WeChatBase):
def __init__(self, language='cn') -> None:
"""微信UI自動化實例
Args:
language (str, optional): 微信客戶端語言版本, 可選: cn簡體中文 cn_t繁體中文 en英文, 默認(rèn)cn, 即簡體中文
"""
self.VERSION = '3.9.8.15'
self.language = language
self.lastmsgid = None
self.listen = dict()
self._checkversion()
self.UiaAPI = uia.WindowControl(ClassName='WeChatMainWndForPC', searchDepth=1)
self._show()
self.SessionItemList = []
MainControl1 = [i for i in self.UiaAPI.GetChildren() if not i.ClassName][0]
MainControl2 = MainControl1.GetChildren()[0]
# 三個布局,導(dǎo)航欄(A)、聊天列表(B)、聊天框(C)
# _______________
# |■|———| -□×|
# | |———| |
# |A| B | C | <--- 微信窗口布局簡圖示意
# | |———|———————|
# |=|———| |
# ———————————————
self.NavigationBox, self.SessionBox, self.ChatBox = MainControl2.GetChildren()
# 初始化導(dǎo)航欄,以A開頭 | self.NavigationBox --> A_xxx
self.A_MyIcon = self.NavigationBox.ButtonControl()
self.A_ChatIcon = self.NavigationBox.ButtonControl(Name=self._lang('聊天'))
self.A_ContactsIcon = self.NavigationBox.ButtonControl(Name=self._lang('通訊錄'))
self.A_FavoritesIcon = self.NavigationBox.ButtonControl(Name=self._lang('收藏'))
self.A_FilesIcon = self.NavigationBox.ButtonControl(Name=self._lang('聊天文件'))
self.A_MomentsIcon = self.NavigationBox.ButtonControl(Name=self._lang('朋友圈'))
self.A_MiniProgram = self.NavigationBox.ButtonControl(Name=self._lang('小程序面板'))
self.A_Phone = self.NavigationBox.ButtonControl(Name=self._lang('手機(jī)'))
self.A_Settings = self.NavigationBox.ButtonControl(Name=self._lang('設(shè)置及其他'))
# 初始化聊天列表,以B開頭
self.B_Search = self.SessionBox.EditControl(Name=self._lang('搜索'))
# 初始化聊天欄,以C開頭
self.C_MsgList = self.ChatBox.ListControl(Name=self._lang('消息'))
self.nickname = self.A_MyIcon.Name
print(f'初始化成功,獲取到已登錄窗口:{self.nickname}')
def _checkversion(self):
self.HWND = FindWindow(classname='WeChatMainWndForPC')
wxpath = GetPathByHwnd(self.HWND)
wxversion = GetVersionByPath(wxpath)
if wxversion != self.VERSION:
Warnings.lightred(self._lang('版本不一致,需要3.9.8.15版本微信,前往下載:https://github.com/tom-snow/wechat-windows-versions/releases?page=2', 'WARNING').format(wxversion, self.VERSION), stacklevel=2)
return False
def _show(self):
self.HWND = FindWindow(classname='WeChatMainWndForPC')
win32gui.ShowWindow(self.HWND, 1)
win32gui.SetWindowPos(self.HWND, -1, 0, 0, 0, 0, 3)
win32gui.SetWindowPos(self.HWND, -2, 0, 0, 0, 0, 3)
self.UiaAPI.SwitchToThisWindow()
def GetSessionAmont(self, SessionItem):
"""獲取聊天對象名和新消息條數(shù)
Args:
SessionItem (uiautomation.ListItemControl): 聊天對象控件
Returns:
sessionname (str): 聊天對象名
amount (int): 新消息條數(shù)
"""
matchobj = re.search('\d+條新消息', SessionItem.Name)
amount = 0
if matchobj:
try:
amount = int([i for i in SessionItem.GetChildren()[0].GetChildren() if type(i) == uia.uiautomation.TextControl][0].Name)
except:
pass
if amount:
sessionname = SessionItem.Name.replace(f'{amount}條新消息','')
else:
sessionname = SessionItem.Name
return sessionname, amount
def CheckNewMessage(self):
"""是否有新消息"""
self._show()
return IsRedPixel(self.A_ChatIcon)
#self.A_ChatIcon 圖像中的紅色像素點來判斷是否有新消息。在很多應(yīng)用中,新消息未讀時圖標(biāo)會顯示為紅色,因此通過檢測圖標(biāo)的紅色像素是否存在,可以間接地確定是否有新消息。
def GetNextNewMessage(self, savepic=False):
"""獲取下一個新消息"""
msgs_ = self.GetAllMessage()
if self.lastmsgid is not None and self.lastmsgid in [i[-1] for i in msgs_[:-1]]:
print('獲取當(dāng)前窗口新消息')
idx = [i[-1] for i in msgs_].index(self.lastmsgid)
MsgItems = self.C_MsgList.GetChildren()[idx+1:]
msgs = self._getmsgs(MsgItems, savepic)
return {self.CurrentChat(): msgs}
elif self.CheckNewMessage():
print('獲取其他窗口新消息')
while True:
self.A_ChatIcon.DoubleClick(simulateMove=False)
sessiondict = self.GetSessionList(newmessage=True)
if sessiondict:
break
for session in sessiondict:
self.ChatWith(session)
MsgItems = self.C_MsgList.GetChildren()[-sessiondict[session]:]
msgs = self._getmsgs(MsgItems, savepic)
self.lastmsgid = msgs[-1][-1]
return {session:msgs}
else:
# print('沒有新消息')
return None
#該函數(shù)用于獲取下一個新消息。首先,它會嘗試獲取當(dāng)前窗口的新消息,如果成功,則返回當(dāng)前聊天窗口的消息列表。
#如果當(dāng)前窗口沒有新消息,它會檢查是否有其他窗口有新消息。如果有,則會逐個點擊聊天圖標(biāo),打開有新消息的聊天窗口,并返回該窗口的消息列表。
#函數(shù)返回一個字典,包含每個聊天窗口的消息列表。如果沒有新消息,則返回None。
def GetAllNewMessage(self):
"""獲取所有新消息"""
newmessages = {}
while True:
if self.CheckNewMessage():
self.A_ChatIcon.DoubleClick(simulateMove=False)
sessiondict = self.GetSessionList(newmessage=True)
for session in sessiondict:
self.ChatWith(session)
newmessages[session] = self.GetAllMessage()[-sessiondict[session]:]
else:
break
self.ChatWith(self._lang('文件傳輸助手'))
return newmessages
def GetSessionList(self, reset=False, newmessage=False):
"""獲取當(dāng)前聊天列表中的所有聊天對象
Args:
reset (bool): 是否重置SessionItemList
newmessage (bool): 是否只獲取有新消息的聊天對象
Returns:
SessionList (dict): 聊天對象列表,鍵為聊天對象名,值為新消息條數(shù)
"""
self.SessionItem = self.SessionBox.ListItemControl()
if reset:
self.SessionItemList = []
SessionList = {}
for i in range(100):
if self.SessionItem.BoundingRectangle.width() != 0:
try:
name, amount = self.GetSessionAmont(self.SessionItem)
except:
break
if name not in self.SessionItemList:
self.SessionItemList.append(name)
if name not in SessionList:
SessionList[name] = amount
self.SessionItem = self.SessionItem.GetNextSiblingControl()
if not self.SessionItem:
break
if newmessage:
return {i:SessionList[i] for i in SessionList if SessionList[i] > 0}
return SessionList
def ChatWith(self, who, notfound: Literal['raise', 'ignore']='ignore'):
'''打開某個聊天框
Args:
who ( str ): 要打開的聊天框好友名; * 最好完整匹配,不完全匹配只會選取搜索框第一個
notfound ( str, optional ): 未找到時的處理方式,可選:raise-拋出異常 ignore-忽略,默認(rèn)ignore
Returns:
chatname ( str ): 匹配值第一個的完整名字
'''
self._show()
sessiondict = self.GetSessionList(True)
if who in list(sessiondict.keys())[:-1]:
if sessiondict[who] > 0:
who1 = f"{who}{sessiondict[who]}條新消息"
else:
who1 = who
self.SessionBox.ListItemControl(Name=who1).Click(simulateMove=False)
return who
self.UiaAPI.SendKeys('{Ctrl}f', waitTime=1)
self.B_Search.SendKeys(who, waitTime=1.5)
SearchResut = self.SessionBox.GetChildren()[1].GetChildren()[1]
firstresult = [i for i in SearchResut.GetChildren()[0].GetChildren() if who in i.Name][0]
if firstresult.Name == f'搜索 {who}':
if len(self.SessionBox.GetChildren()[1].GetChildren()) > 1:
self.B_Search.SendKeys('{Esc}')
if notfound == 'raise':
raise TargetNotFoundError(f'未查詢到目標(biāo):{who}')
elif notfound == 'ignore':
return None
chatname = firstresult.Name
firstresult.Click(simulateMove=False)
return chatname
def SendMsg(self, msg, who=None, clear=True):
"""發(fā)送文本消息
Args:
msg (str): 要發(fā)送的文本消息
who (str): 要發(fā)送給誰,如果為None,則發(fā)送到當(dāng)前聊天頁面。 *最好完整匹配,優(yōu)先使用備注
clear (bool, optional): 是否清除原本的內(nèi)容,
"""
if who in self.listen:
chat = self.listen[who]
chat.SendMsg(msg)
return None
if not msg:
return None
if who:
try:
editbox = self.ChatBox.EditControl(searchDepth=10)
if who in self.CurrentChat() and who in editbox.Name:
pass
else:
self.ChatWith(who)
editbox = self.ChatBox.EditControl(Name=who, searchDepth=10)
except:
self.ChatWith(who)
editbox = self.ChatBox.EditControl(Name=who, searchDepth=10)
else:
editbox = self.ChatBox.EditControl(searchDepth=10)
if clear:
editbox.SendKeys('{Ctrl}a', waitTime=0)
self._show()
if not editbox.HasKeyboardFocus:
editbox.Click(simulateMove=False)
t0 = time.time()
while True:
if time.time() - t0 > 10:
raise TimeoutError(f'發(fā)送消息超時 --> {editbox.Name} - {msg}')
SetClipboardText(msg)
editbox.SendKeys('{Ctrl}v')
if editbox.GetValuePattern().Value:
break
editbox.SendKeys('{Enter}')
def SendFiles(self, filepath, who=None):
"""向當(dāng)前聊天窗口發(fā)送文件
Args:
filepath (str|list): 要復(fù)制文件的絕對路徑
who (str): 要發(fā)送給誰,如果為None,則發(fā)送到當(dāng)前聊天頁面。 *最好完整匹配,優(yōu)先使用備注
Returns:
bool: 是否成功發(fā)送文件
"""
if who in self.listen:
chat = self.listen[who]
chat.SendFiles(filepath)
return None
filelist = []
if isinstance(filepath, str):
if not os.path.exists(filepath):
Warnings.lightred(f'未找到文件:{filepath},無法成功發(fā)送', stacklevel=2)
return False
else:
filelist.append(os.path.realpath(filepath))
elif isinstance(filepath, (list, tuple, set)):
for i in filepath:
if os.path.exists(i):
filelist.append(i)
else:
Warnings.lightred(f'未找到文件:{i}', stacklevel=2)
else:
Warnings.lightred(f'filepath參數(shù)格式錯誤:{type(filepath)},應(yīng)為str、list、tuple、set格式', stacklevel=2)
return False
if filelist:
self._show()
if who:
try:
if who in self.CurrentChat() and who in self.ChatBox.EditControl(searchDepth=10).Name:
pass
else:
self.ChatWith(who)
except:
self.ChatWith(who)
editbox = self.ChatBox.EditControl(Name=who)
else:
editbox = self.ChatBox.EditControl()
editbox.SendKeys('{Ctrl}a', waitTime=0)
t0 = time.time()
while True:
if time.time() - t0 > 10:
raise TimeoutError(f'發(fā)送文件超時 --> {filelist}')
SetClipboardFiles(filelist)
time.sleep(0.2)
editbox.SendKeys('{Ctrl}v')
if editbox.GetValuePattern().Value:
break
editbox.SendKeys('{Enter}')
return True
else:
Warnings.lightred('所有文件都無法成功發(fā)送', stacklevel=2)
return False
def GetAllMessage(self, savepic=False, n=0):
'''獲取當(dāng)前窗口中加載的所有聊天記錄
Args:
savepic (bool): 是否自動保存聊天圖片
Returns:
list: 聊天記錄信息
'''
MsgItems = self.C_MsgList.GetChildren()
msgs = self._getmsgs(MsgItems, savepic)
return msgs
def LoadMoreMessage(self):
"""加載當(dāng)前聊天頁面更多聊天信息
Returns:
bool: 是否成功加載更多聊天信息
"""
loadmore = self.C_MsgList.GetChildren()[0]
loadmore_top = loadmore.BoundingRectangle.top
top = self.C_MsgList.BoundingRectangle.top
while True:
if loadmore.BoundingRectangle.top > top or loadmore.Name == '':
isload = True
break
else:
self.C_MsgList.WheelUp(wheelTimes=10, waitTime=0.1)
if loadmore.BoundingRectangle.top == loadmore_top:
isload = False
break
else:
loadmore_top = loadmore.BoundingRectangle.top
self.C_MsgList.WheelUp(wheelTimes=1, waitTime=0.1)
return isload
def CurrentChat(self):
'''獲取當(dāng)前聊天對象名'''
uia.SetGlobalSearchTimeout(1)
try:
currentname = self.ChatBox.TextControl(searchDepth=15).Name
return currentname
except:
return None
finally:
uia.SetGlobalSearchTimeout(10)
def GetNewFriends(self):
"""獲取新的好友申請列表
Returns:
list: 新的好友申請列表,元素為NewFriendsElement對象,可直接調(diào)用Accept方法
Example:
>>> wx = WeChat()
>>> newfriends = wx.GetNewFriends()
>>> tags = ['標(biāo)簽1', '標(biāo)簽2']
>>> for friend in newfriends:
>>> remark = f'備注{friend.name}'
>>> friend.Accept(remark=remark, tags=tags) # 接受好友請求,并設(shè)置備注和標(biāo)簽
"""
self._show()
self.SwitchToContact()
self.SessionBox.ButtonControl(Name='ContactListItem').Click(simulateMove=False)
NewFriendsList = [NewFriendsElement(i, self) for i in self.ChatBox.ListControl(Name='新的朋友').GetChildren()]
AcceptableNewFriendsList = [i for i in NewFriendsList if i.acceptable]
print(f'獲取到 {len(AcceptableNewFriendsList)} 條新的好友申請')
return AcceptableNewFriendsList
def AddListenChat(self, who, savepic=False):
"""添加監(jiān)聽對象
Args:
who (str): 要監(jiān)聽的聊天對象名
savepic (bool, optional): 是否自動保存聊天圖片,只針對該聊天對象有效
"""
exists = uia.WindowControl(searchDepth=1, ClassName='ChatWnd', Name=who).Exists(maxSearchSeconds=0.1)
if not exists:
self.ChatWith(who)
self.SessionBox.ListItemControl(Name=who).DoubleClick(simulateMove=False)
self.listen[who] = ChatWnd(who, self.language)
self.listen[who].savepic = savepic
def GetListenMessage(self):
"""獲取監(jiān)聽對象的新消息"""
msgs = {}
for who in self.listen:
chat = self.listen[who]
chat._show()
msg = chat.GetNewMessage(savepic=chat.savepic)
# if [i for i in msg if i[0] != 'Self']:
if msg:
msgs[chat] = msg
return msgs
def SwitchToContact(self):
"""切換到通訊錄頁面"""
self._show()
self.A_ContactsIcon.Click(simulateMove=False)
def SwitchToChat(self):
"""切換到聊天頁面"""
self._show()
self.A_ChatIcon.Click(simulateMove=False)
def GetGroupMembers(self):
"""獲取當(dāng)前聊天群成員
Returns:
list: 當(dāng)前聊天群成員列表
"""
ele = self.ChatBox.PaneControl(searchDepth=7, foundIndex=6).ButtonControl(Name='聊天信息')
try:
uia.SetGlobalSearchTimeout(1)
rect = ele.BoundingRectangle
Click(rect)
except:
return
finally:
uia.SetGlobalSearchTimeout(10)
roominfoWnd = self.UiaAPI.WindowControl(ClassName='SessionChatRoomDetailWnd', searchDepth=1)
more = roominfoWnd.ButtonControl(Name='查看更多', searchDepth=8)
try:
uia.SetGlobalSearchTimeout(1)
rect = more.BoundingRectangle
Click(rect)
except:
pass
finally:
uia.SetGlobalSearchTimeout(10)
members = [i.Name for i in roominfoWnd.ListControl(Name='聊天成員').GetChildren()]
while members[-1] in ['添加', '移出']:
members = members[:-1]
roominfoWnd.SendKeys('{Esc}')
return members
def GetAllFriendNicknames(self, keywords=None):
"""獲取所有好友昵稱列表
注:
1. 該方法運(yùn)行時間取決于好友數(shù)量,約每秒6~8個好友的速度
2. 該方法未經(jīng)過大量測試,可能存在未知問題,如有問題請微信群內(nèi)反饋
Args:
keywords (str, optional): 搜索關(guān)鍵詞,只返回包含關(guān)鍵詞的好友昵稱列表
Returns:
list: 所有好友昵稱列表
"""
self._show()
self.SwitchToContact()
self.SessionBox.ListControl(Name="聯(lián)系人").ButtonControl(Name="通訊錄管理").Click(simulateMove=False)
contactwnd = ContactWnd()
if keywords:
contactwnd.Search(keywords)
friends_nicknames = contactwnd.GetAllFriendNicknames()
contactwnd.Close()
self.SwitchToChat()
return friends_nicknames
代碼賞析以及實現(xiàn)原理
初始化:在類的初始化方法中,首先檢查微信是否已經(jīng)運(yùn)行,如果沒有運(yùn)行,則嘗試啟動微信。然后,獲取微信的窗口句柄,并設(shè)置微信窗口的大小和位置。
獲取會話列表:
GetSessionList
?方法用于獲取當(dāng)前聊天列表中的所有聊天對象。它會遍歷聊天列表,并返回每個聊天對象的名稱和新消息數(shù)量。切換聊天對象:
ChatWith
?方法用于切換到指定的聊天對象。它會搜索指定的聊天對象,并將其設(shè)置為當(dāng)前聊天對象。如果找不到指定的聊天對象,它會嘗試搜索并選取搜索結(jié)果中的第一個。發(fā)送消息:
SendMsg
?方法用于向當(dāng)前聊天對象發(fā)送文本消息。它會將消息發(fā)送到當(dāng)前聊天對象的編輯框中,并發(fā)送回車鍵以發(fā)送消息。發(fā)送文件:
SendFiles
?方法用于向當(dāng)前聊天對象發(fā)送文件。它會將文件復(fù)制到剪貼板中,并將其粘貼到當(dāng)前聊天對象的編輯框中,然后發(fā)送回車鍵以發(fā)送文件。獲取聊天記錄:
GetAllMessage
?方法用于獲取當(dāng)前聊天對象的所有聊天記錄。它會遍歷聊天記錄列表,并返回每個聊天記錄的信息。加載更多聊天記錄:
LoadMoreMessage
?方法用于加載當(dāng)前聊天對象的更多聊天記錄。它會滾動聊天記錄列表,并檢查是否已經(jīng)加載了更多的聊天記錄。獲取當(dāng)前聊天對象名稱:
CurrentChat
?方法用于獲取當(dāng)前聊天對象的名稱。獲取新的好友申請列表:
GetNewFriends
?方法用于獲取新的好友申請列表。它會遍歷新的好友申請列表,并返回每個好友申請的信息。添加監(jiān)聽對象:
AddListenChat
?方法用于添加監(jiān)聽對象。它會創(chuàng)建一個新的 ChatWnd 對象,并將其添加到監(jiān)聽對象字典中。獲取監(jiān)聽對象的新消息:
GetListenMessage
?方法用于獲取監(jiān)聽對象的新消息。它會遍歷監(jiān)聽對象字典,并獲取每個監(jiān)聽對象的新消息。切換到通訊錄頁面:
SwitchToContact
?方法用于切換到通訊錄頁面。切換到聊天頁面:SwitchToChat
?方法用于切換到聊天頁面。獲取當(dāng)前聊天群成員:
GetGroupMembers
?方法用于獲取當(dāng)前聊天群的成員列表。它會遍歷聊天群成員列表,并返回每個成員的名稱。文章來源:http://www.zghlxwxcb.cn/news/detail-859431.html獲取所有好友昵稱列表:
GetAllFriendNicknames
?方法用于獲取所有好友的昵稱列表。它會打開通訊錄窗口,并搜索指定的關(guān)鍵詞(如果提供了關(guān)鍵詞)。然后,它會遍歷通訊錄列表,并返回每個好友的昵稱。最后,它會關(guān)閉通訊錄窗口,并切換回聊天頁面。文章來源地址http://www.zghlxwxcb.cn/news/detail-859431.html
到了這里,關(guān)于python控制UI實現(xiàn)桌面微信自動化的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!