当前位置:   article > 正文

python控制UI实现桌面微信自动化_python 操作桌面微信

python 操作桌面微信

Hello,我是新星博主:小恒不会java

背景

使用 wxpy 或者 itchat 这种第三方库通过Python控制自己的微信号,实现很多自动化操作,用的是微信网页版接口,不过随着微信的发展(信息安全等方面愈加重要,这种不符合官方期望出现的东西,很容易就破产。也由于itchat在 python 的 request 请求中,使用到的 headers 都是非常简单的 headers。而且频繁利用到
config.USER_AGENT ,被发现怀疑用脚本封号是当然的

所以我打算用wxauto,用UI控件的形式操作微信(uiautomation实现)

wxauto第三方库介绍

wxauto是某GitHub某位开源的

https://github.com/cluic/wxauto

功能很基础,毕竟是UI控件,实现的功能有

(1)群发消息包括图片,文件

(2)自动回复和添加好友(有小bug,简单修改一下就行)

(3) 监控群聊人员或者个人聊天框

(4)获取微信好友昵称等信息(有bug,我改了一下没成功)

效果如下

wxauto.py代码

  1. """
  2. Author: Cluic
  3. Update: 2024-03-10
  4. Version: 3.9.8.15
  5. """
  6. import uiautomation as uia
  7. from .languages import *
  8. from .utils import *
  9. from .elements import *
  10. from .errors import *
  11. from .color import *
  12. import datetime
  13. import time
  14. import os
  15. import re
  16. try:
  17. from typing import Literal
  18. except:
  19. from typing_extensions import Literal
  20. class WeChat(WeChatBase):
  21. def __init__(self, language='cn') -> None:
  22. """微信UI自动化实例
  23. Args:
  24. language (str, optional): 微信客户端语言版本, 可选: cn简体中文 cn_t繁体中文 en英文, 默认cn, 即简体中文
  25. """
  26. self.VERSION = '3.9.8.15'
  27. self.language = language
  28. self.lastmsgid = None
  29. self.listen = dict()
  30. self._checkversion()
  31. self.UiaAPI = uia.WindowControl(ClassName='WeChatMainWndForPC', searchDepth=1)
  32. self._show()
  33. self.SessionItemList = []
  34. MainControl1 = [i for i in self.UiaAPI.GetChildren() if not i.ClassName][0]
  35. MainControl2 = MainControl1.GetChildren()[0]
  36. # 三个布局,导航栏(A)、聊天列表(B)、聊天框(C)
  37. # _______________
  38. # |■|———| -□×|
  39. # | |———| |
  40. # |A| B | C | <--- 微信窗口布局简图示意
  41. # | |———|———————|
  42. # |=|———| |
  43. # ———————————————
  44. self.NavigationBox, self.SessionBox, self.ChatBox = MainControl2.GetChildren()
  45. # 初始化导航栏,以A开头 | self.NavigationBox --> A_xxx
  46. self.A_MyIcon = self.NavigationBox.ButtonControl()
  47. self.A_ChatIcon = self.NavigationBox.ButtonControl(Name=self._lang('聊天'))
  48. self.A_ContactsIcon = self.NavigationBox.ButtonControl(Name=self._lang('通讯录'))
  49. self.A_FavoritesIcon = self.NavigationBox.ButtonControl(Name=self._lang('收藏'))
  50. self.A_FilesIcon = self.NavigationBox.ButtonControl(Name=self._lang('聊天文件'))
  51. self.A_MomentsIcon = self.NavigationBox.ButtonControl(Name=self._lang('朋友圈'))
  52. self.A_MiniProgram = self.NavigationBox.ButtonControl(Name=self._lang('小程序面板'))
  53. self.A_Phone = self.NavigationBox.ButtonControl(Name=self._lang('手机'))
  54. self.A_Settings = self.NavigationBox.ButtonControl(Name=self._lang('设置及其他'))
  55. # 初始化聊天列表,以B开头
  56. self.B_Search = self.SessionBox.EditControl(Name=self._lang('搜索'))
  57. # 初始化聊天栏,以C开头
  58. self.C_MsgList = self.ChatBox.ListControl(Name=self._lang('消息'))
  59. self.nickname = self.A_MyIcon.Name
  60. print(f'初始化成功,获取到已登录窗口:{self.nickname}')
  61. def _checkversion(self):
  62. self.HWND = FindWindow(classname='WeChatMainWndForPC')
  63. wxpath = GetPathByHwnd(self.HWND)
  64. wxversion = GetVersionByPath(wxpath)
  65. if wxversion != self.VERSION:
  66. 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)
  67. return False
  68. def _show(self):
  69. self.HWND = FindWindow(classname='WeChatMainWndForPC')
  70. win32gui.ShowWindow(self.HWND, 1)
  71. win32gui.SetWindowPos(self.HWND, -1, 0, 0, 0, 0, 3)
  72. win32gui.SetWindowPos(self.HWND, -2, 0, 0, 0, 0, 3)
  73. self.UiaAPI.SwitchToThisWindow()
  74. def GetSessionAmont(self, SessionItem):
  75. """获取聊天对象名和新消息条数
  76. Args:
  77. SessionItem (uiautomation.ListItemControl): 聊天对象控件
  78. Returns:
  79. sessionname (str): 聊天对象名
  80. amount (int): 新消息条数
  81. """
  82. matchobj = re.search('\d+条新消息', SessionItem.Name)
  83. amount = 0
  84. if matchobj:
  85. try:
  86. amount = int([i for i in SessionItem.GetChildren()[0].GetChildren() if type(i) == uia.uiautomation.TextControl][0].Name)
  87. except:
  88. pass
  89. if amount:
  90. sessionname = SessionItem.Name.replace(f'{amount}条新消息','')
  91. else:
  92. sessionname = SessionItem.Name
  93. return sessionname, amount
  94. def CheckNewMessage(self):
  95. """是否有新消息"""
  96. self._show()
  97. return IsRedPixel(self.A_ChatIcon)
  98. #self.A_ChatIcon 图像中的红色像素点来判断是否有新消息。在很多应用中,新消息未读时图标会显示为红色,因此通过检测图标的红色像素是否存在,可以间接地确定是否有新消息。
  99. def GetNextNewMessage(self, savepic=False):
  100. """获取下一个新消息"""
  101. msgs_ = self.GetAllMessage()
  102. if self.lastmsgid is not None and self.lastmsgid in [i[-1] for i in msgs_[:-1]]:
  103. print('获取当前窗口新消息')
  104. idx = [i[-1] for i in msgs_].index(self.lastmsgid)
  105. MsgItems = self.C_MsgList.GetChildren()[idx+1:]
  106. msgs = self._getmsgs(MsgItems, savepic)
  107. return {self.CurrentChat(): msgs}
  108. elif self.CheckNewMessage():
  109. print('获取其他窗口新消息')
  110. while True:
  111. self.A_ChatIcon.DoubleClick(simulateMove=False)
  112. sessiondict = self.GetSessionList(newmessage=True)
  113. if sessiondict:
  114. break
  115. for session in sessiondict:
  116. self.ChatWith(session)
  117. MsgItems = self.C_MsgList.GetChildren()[-sessiondict[session]:]
  118. msgs = self._getmsgs(MsgItems, savepic)
  119. self.lastmsgid = msgs[-1][-1]
  120. return {session:msgs}
  121. else:
  122. # print('没有新消息')
  123. return None
  124. #该函数用于获取下一个新消息。首先,它会尝试获取当前窗口的新消息,如果成功,则返回当前聊天窗口的消息列表。
  125. #如果当前窗口没有新消息,它会检查是否有其他窗口有新消息。如果有,则会逐个点击聊天图标,打开有新消息的聊天窗口,并返回该窗口的消息列表。
  126. #函数返回一个字典,包含每个聊天窗口的消息列表。如果没有新消息,则返回None。
  127. def GetAllNewMessage(self):
  128. """获取所有新消息"""
  129. newmessages = {}
  130. while True:
  131. if self.CheckNewMessage():
  132. self.A_ChatIcon.DoubleClick(simulateMove=False)
  133. sessiondict = self.GetSessionList(newmessage=True)
  134. for session in sessiondict:
  135. self.ChatWith(session)
  136. newmessages[session] = self.GetAllMessage()[-sessiondict[session]:]
  137. else:
  138. break
  139. self.ChatWith(self._lang('文件传输助手'))
  140. return newmessages
  141. def GetSessionList(self, reset=False, newmessage=False):
  142. """获取当前聊天列表中的所有聊天对象
  143. Args:
  144. reset (bool): 是否重置SessionItemList
  145. newmessage (bool): 是否只获取有新消息的聊天对象
  146. Returns:
  147. SessionList (dict): 聊天对象列表,键为聊天对象名,值为新消息条数
  148. """
  149. self.SessionItem = self.SessionBox.ListItemControl()
  150. if reset:
  151. self.SessionItemList = []
  152. SessionList = {}
  153. for i in range(100):
  154. if self.SessionItem.BoundingRectangle.width() != 0:
  155. try:
  156. name, amount = self.GetSessionAmont(self.SessionItem)
  157. except:
  158. break
  159. if name not in self.SessionItemList:
  160. self.SessionItemList.append(name)
  161. if name not in SessionList:
  162. SessionList[name] = amount
  163. self.SessionItem = self.SessionItem.GetNextSiblingControl()
  164. if not self.SessionItem:
  165. break
  166. if newmessage:
  167. return {i:SessionList[i] for i in SessionList if SessionList[i] > 0}
  168. return SessionList
  169. def ChatWith(self, who, notfound: Literal['raise', 'ignore']='ignore'):
  170. '''打开某个聊天框
  171. Args:
  172. who ( str ): 要打开的聊天框好友名; * 最好完整匹配,不完全匹配只会选取搜索框第一个
  173. notfound ( str, optional ): 未找到时的处理方式,可选:raise-抛出异常 ignore-忽略,默认ignore
  174. Returns:
  175. chatname ( str ): 匹配值第一个的完整名字
  176. '''
  177. self._show()
  178. sessiondict = self.GetSessionList(True)
  179. if who in list(sessiondict.keys())[:-1]:
  180. if sessiondict[who] > 0:
  181. who1 = f"{who}{sessiondict[who]}条新消息"
  182. else:
  183. who1 = who
  184. self.SessionBox.ListItemControl(Name=who1).Click(simulateMove=False)
  185. return who
  186. self.UiaAPI.SendKeys('{Ctrl}f', waitTime=1)
  187. self.B_Search.SendKeys(who, waitTime=1.5)
  188. SearchResut = self.SessionBox.GetChildren()[1].GetChildren()[1]
  189. firstresult = [i for i in SearchResut.GetChildren()[0].GetChildren() if who in i.Name][0]
  190. if firstresult.Name == f'搜索 {who}':
  191. if len(self.SessionBox.GetChildren()[1].GetChildren()) > 1:
  192. self.B_Search.SendKeys('{Esc}')
  193. if notfound == 'raise':
  194. raise TargetNotFoundError(f'未查询到目标:{who}')
  195. elif notfound == 'ignore':
  196. return None
  197. chatname = firstresult.Name
  198. firstresult.Click(simulateMove=False)
  199. return chatname
  200. def SendMsg(self, msg, who=None, clear=True):
  201. """发送文本消息
  202. Args:
  203. msg (str): 要发送的文本消息
  204. who (str): 要发送给谁,如果为None,则发送到当前聊天页面。 *最好完整匹配,优先使用备注
  205. clear (bool, optional): 是否清除原本的内容,
  206. """
  207. if who in self.listen:
  208. chat = self.listen[who]
  209. chat.SendMsg(msg)
  210. return None
  211. if not msg:
  212. return None
  213. if who:
  214. try:
  215. editbox = self.ChatBox.EditControl(searchDepth=10)
  216. if who in self.CurrentChat() and who in editbox.Name:
  217. pass
  218. else:
  219. self.ChatWith(who)
  220. editbox = self.ChatBox.EditControl(Name=who, searchDepth=10)
  221. except:
  222. self.ChatWith(who)
  223. editbox = self.ChatBox.EditControl(Name=who, searchDepth=10)
  224. else:
  225. editbox = self.ChatBox.EditControl(searchDepth=10)
  226. if clear:
  227. editbox.SendKeys('{Ctrl}a', waitTime=0)
  228. self._show()
  229. if not editbox.HasKeyboardFocus:
  230. editbox.Click(simulateMove=False)
  231. t0 = time.time()
  232. while True:
  233. if time.time() - t0 > 10:
  234. raise TimeoutError(f'发送消息超时 --> {editbox.Name} - {msg}')
  235. SetClipboardText(msg)
  236. editbox.SendKeys('{Ctrl}v')
  237. if editbox.GetValuePattern().Value:
  238. break
  239. editbox.SendKeys('{Enter}')
  240. def SendFiles(self, filepath, who=None):
  241. """向当前聊天窗口发送文件
  242. Args:
  243. filepath (str|list): 要复制文件的绝对路径
  244. who (str): 要发送给谁,如果为None,则发送到当前聊天页面。 *最好完整匹配,优先使用备注
  245. Returns:
  246. bool: 是否成功发送文件
  247. """
  248. if who in self.listen:
  249. chat = self.listen[who]
  250. chat.SendFiles(filepath)
  251. return None
  252. filelist = []
  253. if isinstance(filepath, str):
  254. if not os.path.exists(filepath):
  255. Warnings.lightred(f'未找到文件:{filepath},无法成功发送', stacklevel=2)
  256. return False
  257. else:
  258. filelist.append(os.path.realpath(filepath))
  259. elif isinstance(filepath, (list, tuple, set)):
  260. for i in filepath:
  261. if os.path.exists(i):
  262. filelist.append(i)
  263. else:
  264. Warnings.lightred(f'未找到文件:{i}', stacklevel=2)
  265. else:
  266. Warnings.lightred(f'filepath参数格式错误:{type(filepath)},应为str、list、tuple、set格式', stacklevel=2)
  267. return False
  268. if filelist:
  269. self._show()
  270. if who:
  271. try:
  272. if who in self.CurrentChat() and who in self.ChatBox.EditControl(searchDepth=10).Name:
  273. pass
  274. else:
  275. self.ChatWith(who)
  276. except:
  277. self.ChatWith(who)
  278. editbox = self.ChatBox.EditControl(Name=who)
  279. else:
  280. editbox = self.ChatBox.EditControl()
  281. editbox.SendKeys('{Ctrl}a', waitTime=0)
  282. t0 = time.time()
  283. while True:
  284. if time.time() - t0 > 10:
  285. raise TimeoutError(f'发送文件超时 --> {filelist}')
  286. SetClipboardFiles(filelist)
  287. time.sleep(0.2)
  288. editbox.SendKeys('{Ctrl}v')
  289. if editbox.GetValuePattern().Value:
  290. break
  291. editbox.SendKeys('{Enter}')
  292. return True
  293. else:
  294. Warnings.lightred('所有文件都无法成功发送', stacklevel=2)
  295. return False
  296. def GetAllMessage(self, savepic=False, n=0):
  297. '''获取当前窗口中加载的所有聊天记录
  298. Args:
  299. savepic (bool): 是否自动保存聊天图片
  300. Returns:
  301. list: 聊天记录信息
  302. '''
  303. MsgItems = self.C_MsgList.GetChildren()
  304. msgs = self._getmsgs(MsgItems, savepic)
  305. return msgs
  306. def LoadMoreMessage(self):
  307. """加载当前聊天页面更多聊天信息
  308. Returns:
  309. bool: 是否成功加载更多聊天信息
  310. """
  311. loadmore = self.C_MsgList.GetChildren()[0]
  312. loadmore_top = loadmore.BoundingRectangle.top
  313. top = self.C_MsgList.BoundingRectangle.top
  314. while True:
  315. if loadmore.BoundingRectangle.top > top or loadmore.Name == '':
  316. isload = True
  317. break
  318. else:
  319. self.C_MsgList.WheelUp(wheelTimes=10, waitTime=0.1)
  320. if loadmore.BoundingRectangle.top == loadmore_top:
  321. isload = False
  322. break
  323. else:
  324. loadmore_top = loadmore.BoundingRectangle.top
  325. self.C_MsgList.WheelUp(wheelTimes=1, waitTime=0.1)
  326. return isload
  327. def CurrentChat(self):
  328. '''获取当前聊天对象名'''
  329. uia.SetGlobalSearchTimeout(1)
  330. try:
  331. currentname = self.ChatBox.TextControl(searchDepth=15).Name
  332. return currentname
  333. except:
  334. return None
  335. finally:
  336. uia.SetGlobalSearchTimeout(10)
  337. def GetNewFriends(self):
  338. """获取新的好友申请列表
  339. Returns:
  340. list: 新的好友申请列表,元素为NewFriendsElement对象,可直接调用Accept方法
  341. Example:
  342. >>> wx = WeChat()
  343. >>> newfriends = wx.GetNewFriends()
  344. >>> tags = ['标签1', '标签2']
  345. >>> for friend in newfriends:
  346. >>> remark = f'备注{friend.name}'
  347. >>> friend.Accept(remark=remark, tags=tags) # 接受好友请求,并设置备注和标签
  348. """
  349. self._show()
  350. self.SwitchToContact()
  351. self.SessionBox.ButtonControl(Name='ContactListItem').Click(simulateMove=False)
  352. NewFriendsList = [NewFriendsElement(i, self) for i in self.ChatBox.ListControl(Name='新的朋友').GetChildren()]
  353. AcceptableNewFriendsList = [i for i in NewFriendsList if i.acceptable]
  354. print(f'获取到 {len(AcceptableNewFriendsList)} 条新的好友申请')
  355. return AcceptableNewFriendsList
  356. def AddListenChat(self, who, savepic=False):
  357. """添加监听对象
  358. Args:
  359. who (str): 要监听的聊天对象名
  360. savepic (bool, optional): 是否自动保存聊天图片,只针对该聊天对象有效
  361. """
  362. exists = uia.WindowControl(searchDepth=1, ClassName='ChatWnd', Name=who).Exists(maxSearchSeconds=0.1)
  363. if not exists:
  364. self.ChatWith(who)
  365. self.SessionBox.ListItemControl(Name=who).DoubleClick(simulateMove=False)
  366. self.listen[who] = ChatWnd(who, self.language)
  367. self.listen[who].savepic = savepic
  368. def GetListenMessage(self):
  369. """获取监听对象的新消息"""
  370. msgs = {}
  371. for who in self.listen:
  372. chat = self.listen[who]
  373. chat._show()
  374. msg = chat.GetNewMessage(savepic=chat.savepic)
  375. # if [i for i in msg if i[0] != 'Self']:
  376. if msg:
  377. msgs[chat] = msg
  378. return msgs
  379. def SwitchToContact(self):
  380. """切换到通讯录页面"""
  381. self._show()
  382. self.A_ContactsIcon.Click(simulateMove=False)
  383. def SwitchToChat(self):
  384. """切换到聊天页面"""
  385. self._show()
  386. self.A_ChatIcon.Click(simulateMove=False)
  387. def GetGroupMembers(self):
  388. """获取当前聊天群成员
  389. Returns:
  390. list: 当前聊天群成员列表
  391. """
  392. ele = self.ChatBox.PaneControl(searchDepth=7, foundIndex=6).ButtonControl(Name='聊天信息')
  393. try:
  394. uia.SetGlobalSearchTimeout(1)
  395. rect = ele.BoundingRectangle
  396. Click(rect)
  397. except:
  398. return
  399. finally:
  400. uia.SetGlobalSearchTimeout(10)
  401. roominfoWnd = self.UiaAPI.WindowControl(ClassName='SessionChatRoomDetailWnd', searchDepth=1)
  402. more = roominfoWnd.ButtonControl(Name='查看更多', searchDepth=8)
  403. try:
  404. uia.SetGlobalSearchTimeout(1)
  405. rect = more.BoundingRectangle
  406. Click(rect)
  407. except:
  408. pass
  409. finally:
  410. uia.SetGlobalSearchTimeout(10)
  411. members = [i.Name for i in roominfoWnd.ListControl(Name='聊天成员').GetChildren()]
  412. while members[-1] in ['添加', '移出']:
  413. members = members[:-1]
  414. roominfoWnd.SendKeys('{Esc}')
  415. return members
  416. def GetAllFriendNicknames(self, keywords=None):
  417. """获取所有好友昵称列表
  418. 注:
  419. 1. 该方法运行时间取决于好友数量,约每秒6~8个好友的速度
  420. 2. 该方法未经过大量测试,可能存在未知问题,如有问题请微信群内反馈
  421. Args:
  422. keywords (str, optional): 搜索关键词,只返回包含关键词的好友昵称列表
  423. Returns:
  424. list: 所有好友昵称列表
  425. """
  426. self._show()
  427. self.SwitchToContact()
  428. self.SessionBox.ListControl(Name="联系人").ButtonControl(Name="通讯录管理").Click(simulateMove=False)
  429. contactwnd = ContactWnd()
  430. if keywords:
  431. contactwnd.Search(keywords)
  432. friends_nicknames = contactwnd.GetAllFriendNicknames()
  433. contactwnd.Close()
  434. self.SwitchToChat()
  435. return friends_nicknames

代码赏析以及实现原理

  1. 初始化:在类的初始化方法中,首先检查微信是否已经运行,如果没有运行,则尝试启动微信。然后,获取微信的窗口句柄,并设置微信窗口的大小和位置。

  2. 获取会话列表:GetSessionList 方法用于获取当前聊天列表中的所有聊天对象。它会遍历聊天列表,并返回每个聊天对象的名称和新消息数量。

  3. 切换聊天对象:ChatWith 方法用于切换到指定的聊天对象。它会搜索指定的聊天对象,并将其设置为当前聊天对象。如果找不到指定的聊天对象,它会尝试搜索并选取搜索结果中的第一个。

  4. 发送消息:SendMsg 方法用于向当前聊天对象发送文本消息。它会将消息发送到当前聊天对象的编辑框中,并发送回车键以发送消息。

  5. 发送文件:SendFiles 方法用于向当前聊天对象发送文件。它会将文件复制到剪贴板中,并将其粘贴到当前聊天对象的编辑框中,然后发送回车键以发送文件。

  6. 获取聊天记录:GetAllMessage 方法用于获取当前聊天对象的所有聊天记录。它会遍历聊天记录列表,并返回每个聊天记录的信息。

  7. 加载更多聊天记录:LoadMoreMessage 方法用于加载当前聊天对象的更多聊天记录。它会滚动聊天记录列表,并检查是否已经加载了更多的聊天记录。

  8. 获取当前聊天对象名称:CurrentChat 方法用于获取当前聊天对象的名称。

  9. 获取新的好友申请列表:GetNewFriends 方法用于获取新的好友申请列表。它会遍历新的好友申请列表,并返回每个好友申请的信息。

  10. 添加监听对象:AddListenChat 方法用于添加监听对象。它会创建一个新的 ChatWnd 对象,并将其添加到监听对象字典中。

  11. 获取监听对象的新消息:GetListenMessage 方法用于获取监听对象的新消息。它会遍历监听对象字典,并获取每个监听对象的新消息。

  12. 切换到通讯录页面:SwitchToContact 方法用于切换到通讯录页面。切换到聊天页面:SwitchToChat 方法用于切换到聊天页面。

  13. 获取当前聊天群成员:GetGroupMembers 方法用于获取当前聊天群的成员列表。它会遍历聊天群成员列表,并返回每个成员的名称。

  14. 获取所有好友昵称列表:GetAllFriendNicknames 方法用于获取所有好友的昵称列表。它会打开通讯录窗口,并搜索指定的关键词(如果提供了关键词)。然后,它会遍历通讯录列表,并返回每个好友的昵称。最后,它会关闭通讯录窗口,并切换回聊天页面。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/515430
推荐阅读
相关标签
  

闽ICP备14008679号